From 83816fa1049dd0bae2c8e78e15a0bd1470122e68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl-Gerhard=20Lindesva=CC=88rd?= Date: Tue, 12 Mar 2024 22:31:54 +0100 Subject: [PATCH] make onboarding + create client easier --- apps/dashboard/package.json | 1 + .../[projectId]/create-client.tsx | 252 ------------------ .../[projectId]/layout-menu.tsx | 7 +- .../[organizationId]/[projectId]/page.tsx | 3 +- .../[projectId]/settings/profile/logout.tsx | 10 +- .../(app)/[organizationId]/create-project.tsx | 33 --- .../src/app/(app)/[organizationId]/page.tsx | 6 +- .../src/app/(app)/create-organization.tsx | 182 +++++++++---- .../clients/create-client-success.tsx | 47 ++++ apps/dashboard/src/components/ui/input.tsx | 2 + apps/dashboard/src/components/ui/tabs.tsx | 53 ++++ apps/dashboard/src/modals/AddClient.tsx | 183 +++++-------- .../src/server/api/routers/client.ts | 40 +-- .../src/server/api/routers/onboarding.ts | 26 +- .../src/server/api/routers/reference.ts | 11 +- apps/docs/src/pages/docs/nextjs.mdx | 2 + pnpm-lock.yaml | 31 +++ 17 files changed, 383 insertions(+), 506 deletions(-) delete mode 100644 apps/dashboard/src/app/(app)/[organizationId]/[projectId]/create-client.tsx create mode 100644 apps/dashboard/src/components/clients/create-client-success.tsx create mode 100644 apps/dashboard/src/components/ui/tabs.tsx diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 2799069e..40a0519c 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -33,6 +33,7 @@ "@radix-ui/react-progress": "^1.0.3", "@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-toggle": "^1.0.3", "@radix-ui/react-toggle-group": "^1.0.4", diff --git a/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/create-client.tsx b/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/create-client.tsx deleted file mode 100644 index 71757a87..00000000 --- a/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/create-client.tsx +++ /dev/null @@ -1,252 +0,0 @@ -'use client'; - -import { useEffect, useState } from 'react'; -import { Button } from '@/components/ui/button'; -import { CheckboxInput } from '@/components/ui/checkbox'; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { useAppParams } from '@/hooks/useAppParams'; -import { clipboard } from '@/utils/clipboard'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { Copy, SaveIcon } from 'lucide-react'; -import Link from 'next/link'; -import { useRouter } from 'next/navigation'; -import type { SubmitHandler } from 'react-hook-form'; -import { Controller, useForm, useWatch } from 'react-hook-form'; -import { toast } from 'sonner'; -import { z } from 'zod'; - -import { api, handleError } from '../../../_trpc/client'; - -const validation = z.object({ - name: z.string().min(1), - domain: z.string().optional(), - withSecret: z.boolean().optional(), -}); - -type IForm = z.infer; - -export function CreateClient() { - const [open, setOpen] = useState(false); - const { organizationId, projectId } = useAppParams(); - const clients = api.client.list.useQuery({ - organizationId, - }); - const clientsCount = clients.data?.length; - - useEffect(() => { - if (clientsCount === 0) { - setOpen(true); - } - }, [clientsCount]); - - const router = useRouter(); - const form = useForm({ - resolver: zodResolver(validation), - defaultValues: { - withSecret: false, - name: '', - domain: '', - }, - }); - const mutation = api.client.create2.useMutation({ - onError: handleError, - onSuccess() { - toast.success('Client created'); - router.refresh(); - }, - }); - const onSubmit: SubmitHandler = (values) => { - mutation.mutate({ - name: values.name, - domain: values.withSecret ? undefined : values.domain, - organizationId, - projectId, - }); - }; - - const watch = useWatch({ - control: form.control, - name: 'withSecret', - }); - - return ( - - {mutation.isSuccess ? ( - <> - setOpen(false)} - > - - Success - - {mutation.data.clientSecret - ? 'Use your client id and secret with our SDK to send events to us. ' - : 'Use your client id with our SDK to send events to us. '} - See our{' '} - - documentation - - - -
- - {mutation.data.clientSecret ? ( - - ) : ( -
- -
- {mutation.data.cors} -
-
- You can update cors settings{' '} - - here - -
-
- )} -
- - - -
- - ) : ( - <> - setOpen(false)} - > - - Let's connect - - Create a client so you can start send events to us 🚀 - - -
-
-
- - -
- ( - { - field.onChange(!checked); - }} - > - This is a website - - )} - /> -
- - -
-
- - - - -
-
- - )} -
- ); -} - -//
-//
-// Select your framework and we'll generate a client for you. -//
-//
-// -// -// -// -// -// -//
-//
diff --git a/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/layout-menu.tsx b/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/layout-menu.tsx index f0028c59..660ae7c3 100644 --- a/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/layout-menu.tsx +++ b/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/layout-menu.tsx @@ -4,11 +4,10 @@ import { useEffect } from 'react'; import { useAppParams } from '@/hooks/useAppParams'; import { cn } from '@/utils/cn'; import { useUser } from '@clerk/nextjs'; -import type { IServiceDashboards } from '@openpanel/db'; import { + BookmarkIcon, BuildingIcon, CogIcon, - DotIcon, GanttChartIcon, KeySquareIcon, LayoutPanelTopIcon, @@ -21,6 +20,8 @@ import type { LucideProps } from 'lucide-react'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; +import type { IServiceDashboards } from '@openpanel/db'; + function LinkWithIcon({ href, icon: Icon, @@ -127,7 +128,7 @@ export default function LayoutMenu({ dashboards }: LayoutMenuProps) { href={`/${params.organizationId}/${projectId}/settings/profile`} /> diff --git a/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/page.tsx b/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/page.tsx index 33bfc9fe..737aa546 100644 --- a/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/page.tsx +++ b/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/page.tsx @@ -10,10 +10,10 @@ import OverviewTopGeo from '@/components/overview/overview-top-geo'; import OverviewTopPages from '@/components/overview/overview-top-pages'; import OverviewTopSources from '@/components/overview/overview-top-sources'; import { getExists } from '@/server/pageExists'; + import { db } from '@openpanel/db'; import OverviewMetrics from '../../../../components/overview/overview-metrics'; -import { CreateClient } from './create-client'; import { StickyBelowHeader } from './layout-sticky-below-header'; import { OverviewReportRange } from './overview-sticky-header'; @@ -38,7 +38,6 @@ export default async function Page({ return ( -
diff --git a/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/settings/profile/logout.tsx b/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/settings/profile/logout.tsx index e4e14826..a599d5f4 100644 --- a/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/settings/profile/logout.tsx +++ b/apps/dashboard/src/app/(app)/[organizationId]/[projectId]/settings/profile/logout.tsx @@ -1,19 +1,23 @@ 'use client'; +import { buttonVariants } from '@/components/ui/button'; import { Widget, WidgetBody, WidgetHead } from '@/components/Widget'; import { SignOutButton } from '@clerk/nextjs'; export function Logout() { return ( - - Sad part + + Sad part

Sometime's you need to go. See you next time

- +
); diff --git a/apps/dashboard/src/app/(app)/[organizationId]/create-project.tsx b/apps/dashboard/src/app/(app)/[organizationId]/create-project.tsx index 59c132a8..8d585245 100644 --- a/apps/dashboard/src/app/(app)/[organizationId]/create-project.tsx +++ b/apps/dashboard/src/app/(app)/[organizationId]/create-project.tsx @@ -2,7 +2,6 @@ import { LogoSquare } from '@/components/Logo'; import { Button } from '@/components/ui/button'; -import { Checkbox } from '@/components/ui/checkbox'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { useAppParams } from '@/hooks/useAppParams'; @@ -79,35 +78,3 @@ export function CreateProject() { ); } - -//
-//
-// Select your framework and we'll generate a client for you. -//
-//
-// -// -// -// -// -// -//
-//
diff --git a/apps/dashboard/src/app/(app)/[organizationId]/page.tsx b/apps/dashboard/src/app/(app)/[organizationId]/page.tsx index ea705c60..f4e5738e 100644 --- a/apps/dashboard/src/app/(app)/[organizationId]/page.tsx +++ b/apps/dashboard/src/app/(app)/[organizationId]/page.tsx @@ -30,7 +30,7 @@ export default async function Page({ params: { organizationId } }: PageProps) { const isAccepted = await isWaitlistUserAccepted(); if (!isAccepted) { return ( -
+

Not quite there yet

@@ -46,7 +46,7 @@ export default async function Page({ params: { organizationId } }: PageProps) { if (projects.length === 0) { return ( -
+
@@ -59,7 +59,7 @@ export default async function Page({ params: { organizationId } }: PageProps) { } return ( -
+

Select project

{projects.map((item) => ( diff --git a/apps/dashboard/src/app/(app)/create-organization.tsx b/apps/dashboard/src/app/(app)/create-organization.tsx index 75503829..ea235757 100644 --- a/apps/dashboard/src/app/(app)/create-organization.tsx +++ b/apps/dashboard/src/app/(app)/create-organization.tsx @@ -1,11 +1,16 @@ 'use client'; +import { useState } from 'react'; +import { CreateClientSuccess } from '@/components/clients/create-client-success'; import { LogoSquare } from '@/components/Logo'; -import { Button } from '@/components/ui/button'; +import { Button, buttonVariants } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { cn } from '@/utils/cn'; import { zodResolver } from '@hookform/resolvers/zod'; -import { SaveIcon } from 'lucide-react'; +import { SaveIcon, WallpaperIcon } from 'lucide-react'; +import Link from 'next/link'; import { useRouter } from 'next/navigation'; import type { SubmitHandler } from 'react-hook-form'; import { useForm } from 'react-hook-form'; @@ -13,10 +18,21 @@ import { z } from 'zod'; import { api, handleError } from '../_trpc/client'; -const validation = z.object({ - organization: z.string().min(4), - project: z.string().optional(), -}); +const validation = z + .object({ + organization: z.string().min(3), + project: z.string().min(3), + cors: z.string().nullable(), + tab: z.string(), + }) + .refine( + (data) => + data.tab === 'other' || (data.tab === 'website' && data.cors !== ''), + { + message: 'Cors is required', + path: ['cors'], + } + ); type IForm = z.infer; @@ -24,63 +40,117 @@ export function CreateOrganization() { const router = useRouter(); const form = useForm({ resolver: zodResolver(validation), + defaultValues: { + organization: '', + project: '', + cors: '', + tab: 'website', + }, }); const mutation = api.onboarding.organziation.useMutation({ onError: handleError, - onSuccess({ organization, project }) { - let url = `/${organization.slug}`; - if (project) { - url += `/${project.id}`; - } - router.replace(url); - }, }); const onSubmit: SubmitHandler = (values) => { - mutation.mutate(values); + mutation.mutate({ + ...values, + cors: values.tab === 'website' ? values.cors : null, + }); }; - return ( - <> -
- -

Welcome to Openpanel

-
- Create your organization below (can be personal or a company) and - optionally your first project 🤠 + + if (mutation.isSuccess && mutation.data.client) { + return ( +
+ +

Nice job!

+
+ You're ready to start using our SDK. Save the client ID and secret (if + you have any) +
+ +
+ + Read docs + +
-
-
- - -
-
- - -
-
- -
-
- + ); + } + + return ( +
+ +

Welcome to Openpanel

+
+ Create your organization below (can be personal or a company) and your + first project. +
+
+
+ + +
+
+ + +
+ + form.setValue('tab', val)} + className="h-28" + > + + Website + Other + + + + + + +
+ 🔑 You will get a secret to use for your API requests. +
+
+
+ +
+ +
+
+
); } diff --git a/apps/dashboard/src/components/clients/create-client-success.tsx b/apps/dashboard/src/components/clients/create-client-success.tsx new file mode 100644 index 00000000..ba723e81 --- /dev/null +++ b/apps/dashboard/src/components/clients/create-client-success.tsx @@ -0,0 +1,47 @@ +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { clipboard } from '@/utils/clipboard'; +import { Copy, RocketIcon } from 'lucide-react'; +import Link from 'next/link'; + +import type { IServiceClient } from '@openpanel/db'; + +import { Label } from '../ui/label'; + +type Props = IServiceClient; + +export function CreateClientSuccess({ id, secret, cors }: Props) { + return ( +
+ + {secret ? ( + + ) : ( +
+ +
+ {cors} +
+
+ )} + + + Get started! + + Read our documentation to get started. Easy peasy! + + +
+ ); +} diff --git a/apps/dashboard/src/components/ui/input.tsx b/apps/dashboard/src/components/ui/input.tsx index 772c9e0e..0a7ae2e3 100644 --- a/apps/dashboard/src/components/ui/input.tsx +++ b/apps/dashboard/src/components/ui/input.tsx @@ -29,6 +29,8 @@ const Input = React.forwardRef( ({ className, error, type, size, ...props }, ref) => { return ( , + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/apps/dashboard/src/modals/AddClient.tsx b/apps/dashboard/src/modals/AddClient.tsx index d700145e..60b4a690 100644 --- a/apps/dashboard/src/modals/AddClient.tsx +++ b/apps/dashboard/src/modals/AddClient.tsx @@ -1,38 +1,42 @@ 'use client'; -import { useEffect } from 'react'; import { api, handleError } from '@/app/_trpc/client'; -import { Button } from '@/components/ui/button'; -import { CheckboxInput } from '@/components/ui/checkbox'; +import { CreateClientSuccess } from '@/components/clients/create-client-success'; +import { Button, buttonVariants } from '@/components/ui/button'; import { Combobox } from '@/components/ui/combobox'; -import { - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog'; +import { DialogFooter } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { useAppParams } from '@/hooks/useAppParams'; -import { clipboard } from '@/utils/clipboard'; +import { cn } from '@/utils/cn'; import { zodResolver } from '@hookform/resolvers/zod'; -import { Copy, SaveIcon } from 'lucide-react'; +import { SaveIcon, WallpaperIcon } from 'lucide-react'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; import type { SubmitHandler } from 'react-hook-form'; -import { Controller, useForm, useWatch } from 'react-hook-form'; +import { Controller, useForm } from 'react-hook-form'; import { toast } from 'sonner'; import { z } from 'zod'; import { popModal } from '.'; import { ModalContent, ModalHeader } from './Modal/Container'; -const validation = z.object({ - name: z.string().min(1), - domain: z.string().optional(), - withSecret: z.boolean().optional(), - projectId: z.string(), -}); +const validation = z + .object({ + name: z.string().min(1), + cors: z.string().nullable(), + tab: z.string(), + projectId: z.string(), + }) + .refine( + (data) => + data.tab === 'other' || (data.tab === 'website' && data.cors !== ''), + { + message: 'Cors is required', + path: ['cors'], + } + ); type IForm = z.infer; @@ -42,13 +46,13 @@ export default function AddClient() { const form = useForm({ resolver: zodResolver(validation), defaultValues: { - withSecret: false, name: '', - domain: '', + cors: '', + tab: 'website', projectId, }, }); - const mutation = api.client.create2.useMutation({ + const mutation = api.client.create.useMutation({ onError: handleError, onSuccess() { toast.success('Client created'); @@ -61,81 +65,30 @@ export default function AddClient() { const onSubmit: SubmitHandler = (values) => { mutation.mutate({ name: values.name, - domain: values.withSecret ? undefined : values.domain, + cors: values.tab === 'website' ? values.cors : null, projectId: values.projectId, organizationId, }); }; - const watch = useWatch({ - control: form.control, - name: 'withSecret', - }); - return ( {mutation.isSuccess ? ( <> - - {mutation.data.clientSecret - ? 'Use your client id and secret with our SDK to send events to us. ' - : 'Use your client id with our SDK to send events to us. '} - See our{' '} - - documentation - - - } - /> -
- - {mutation.data.clientSecret ? ( - - ) : ( -
- -
- {mutation.data.cors} -
-
- You can update cors settings{' '} - - here - -
-
- )} -
- - - +
) : ( <> @@ -144,37 +97,6 @@ export default function AddClient() { className="flex flex-col gap-4" onSubmit={form.handleSubmit(onSubmit)} > -
- - -
- ( - { - field.onChange(!checked); - }} - > - This is a website - - )} - /> -
- - -
+
+ + +
+ form.setValue('tab', val)} + className="h-28" + > + + Website + Other + + + + + + +
+ 🔑 You will get a secret to use for your API requests. +
+
+