diff --git a/apps/dashboard/src/app/(onboarding)/onboarding-layout.tsx b/apps/dashboard/src/app/(onboarding)/onboarding-layout.tsx
index 0257e364..ef4a5cba 100644
--- a/apps/dashboard/src/app/(onboarding)/onboarding-layout.tsx
+++ b/apps/dashboard/src/app/(onboarding)/onboarding-layout.tsx
@@ -25,7 +25,7 @@ const OnboardingLayout = ({
return (
-
{title}
+ {title}
{description}
diff --git a/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/connect-app.tsx b/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/connect-app.tsx
new file mode 100644
index 00000000..3ad590e4
--- /dev/null
+++ b/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/connect-app.tsx
@@ -0,0 +1,59 @@
+import { pushModal } from '@/modals';
+import { SmartphoneIcon } from 'lucide-react';
+
+import type { IServiceClient } from '@openpanel/db';
+import { frameworks } from '@openpanel/sdk-info';
+
+type Props = {
+ client: IServiceClient | null;
+};
+
+const ConnectApp = ({ client }: Props) => {
+ return (
+
+
+
+ App
+
+
+ Pick a framework below to get started.
+
+
+ {frameworks.app.map((framework) => (
+
+ ))}
+
+
+ Missing a framework?{' '}
+
+ Let us know!
+
+
+
+ );
+};
+
+export default ConnectApp;
diff --git a/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/connect-backend.tsx b/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/connect-backend.tsx
new file mode 100644
index 00000000..41e150ae
--- /dev/null
+++ b/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/connect-backend.tsx
@@ -0,0 +1,59 @@
+import { pushModal } from '@/modals';
+import { ServerIcon } from 'lucide-react';
+
+import type { IServiceClient } from '@openpanel/db';
+import { frameworks } from '@openpanel/sdk-info';
+
+type Props = {
+ client: IServiceClient | null;
+};
+
+const ConnectBackend = ({ client }: Props) => {
+ return (
+
+
+
+ Backend
+
+
+ Pick a framework below to get started.
+
+
+ {frameworks.backend.map((framework) => (
+
+ ))}
+
+
+ Missing a framework?{' '}
+
+ Let us know!
+
+
+
+ );
+};
+
+export default ConnectBackend;
diff --git a/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/connect-web.tsx b/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/connect-web.tsx
new file mode 100644
index 00000000..9b89d0f9
--- /dev/null
+++ b/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/connect-web.tsx
@@ -0,0 +1,59 @@
+import { pushModal } from '@/modals';
+import { MonitorIcon } from 'lucide-react';
+
+import type { IServiceClient } from '@openpanel/db';
+import { frameworks } from '@openpanel/sdk-info';
+
+type Props = {
+ client: IServiceClient | null;
+};
+
+const ConnectWeb = ({ client }: Props) => {
+ return (
+
+
+
+ Website
+
+
+ Pick a framework below to get started.
+
+
+ {frameworks.website.map((framework) => (
+
+ ))}
+
+
+ Missing a framework?{' '}
+
+ Let us know!
+
+
+
+ );
+};
+
+export default ConnectWeb;
diff --git a/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/onboarding-connect.tsx b/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/onboarding-connect.tsx
new file mode 100644
index 00000000..85e1c657
--- /dev/null
+++ b/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/onboarding-connect.tsx
@@ -0,0 +1,58 @@
+'use client';
+
+import { ButtonContainer } from '@/components/button-container';
+import { LinkButton } from '@/components/ui/button';
+
+import type { IServiceProjectWithClients } from '@openpanel/db';
+
+import OnboardingLayout, {
+ OnboardingDescription,
+} from '../../../onboarding-layout';
+import ConnectApp from './connect-app';
+import ConnectBackend from './connect-backend';
+import ConnectWeb from './connect-web';
+
+type Props = {
+ project: IServiceProjectWithClients;
+};
+
+const Connect = ({ project }: Props) => {
+ const client = project.clients[0];
+
+ if (!client) {
+ return
Hmm, something fishy is going on. Please reload the page.
;
+ }
+
+ return (
+
+ Let's connect your data sources to OpenPanel
+
+ }
+ >
+ {project.types.map((type) => {
+ const Component = {
+ website: ConnectWeb,
+ app: ConnectApp,
+ backend: ConnectBackend,
+ }[type];
+
+ return ;
+ })}
+
+
+
+ Next
+
+
+
+ );
+};
+
+export default Connect;
diff --git a/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/page.tsx b/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/page.tsx
new file mode 100644
index 00000000..c993d826
--- /dev/null
+++ b/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/page.tsx
@@ -0,0 +1,34 @@
+import { cookies } from 'next/headers';
+
+import { getCurrentOrganizations, getProjectWithClients } from '@openpanel/db';
+
+import OnboardingConnect from './onboarding-connect';
+
+type Props = {
+ params: {
+ projectId: string;
+ };
+};
+
+const Connect = async ({ params: { projectId } }: Props) => {
+ const orgs = await getCurrentOrganizations();
+ const organizationSlug = orgs[0]?.slug;
+ if (!organizationSlug) {
+ throw new Error('No organization found');
+ }
+ const project = await getProjectWithClients(projectId);
+ const clientSecret = cookies().get('onboarding_client_secret')?.value ?? null;
+
+ if (!project) {
+ return
Hmm, something fishy is going on. Please reload the page.
;
+ }
+
+ // set visible client secret from cookie
+ if (clientSecret && project.clients[0]) {
+ project.clients[0].secret = clientSecret;
+ }
+
+ return
;
+};
+
+export default Connect;
diff --git a/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/verify/onboarding-verify-listener.tsx b/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/verify/onboarding-verify-listener.tsx
new file mode 100644
index 00000000..bd998616
--- /dev/null
+++ b/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/verify/onboarding-verify-listener.tsx
@@ -0,0 +1,127 @@
+'use client';
+
+import { useState } from 'react';
+import { Badge } from '@/components/ui/badge';
+import useWS from '@/hooks/useWS';
+import { pushModal } from '@/modals';
+import { cn } from '@/utils/cn';
+import { timeAgo } from '@/utils/date';
+import { CheckCircle2Icon, CheckIcon, Loader2 } from 'lucide-react';
+
+import type {
+ IServiceClient,
+ IServiceCreateEventPayload,
+ IServiceProject,
+} from '@openpanel/db';
+
+type Props = {
+ project: IServiceProject;
+ client: IServiceClient | null;
+ events: IServiceCreateEventPayload[];
+ onVerified: (verified: boolean) => void;
+};
+
+const VerifyListener = ({ client, events: _events, onVerified }: Props) => {
+ const [events, setEvents] = useState
(
+ _events ?? []
+ );
+ useWS(
+ `/live/events/${client?.projectId}`,
+ (data) => {
+ setEvents((prev) => [...prev, data]);
+ onVerified(true);
+ }
+ );
+
+ const isConnected = events.length > 0;
+
+ const renderBadge = () => {
+ if (isConnected) {
+ return Connected;
+ }
+
+ return Not connected;
+ };
+ const renderIcon = () => {
+ if (isConnected) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+ };
+
+ return (
+
+
+ {client?.name}
+
+
+ Connection status: {renderBadge()}
+
+
+
+ {renderIcon()}
+
+
+ {isConnected ? 'Success' : 'Waiting for events'}
+
+ {isConnected ? (
+
+ {events.length > 5 && (
+
+ {' '}
+ {events.length - 5} more events
+
+ )}
+ {events.slice(-5).map((event, index) => (
+
+ {' '}
+ {event.name}{' '}
+
+ {timeAgo(event.createdAt, 'round')}
+
+
+ ))}
+
+ ) : (
+
+ Verify that your events works before submitting any changes to App
+ Store/Google Play
+
+ )}
+
+
+
+
+ You can{' '}
+ {' '}
+ if you are having issues connecting your app.
+
+
+ );
+};
+
+export default VerifyListener;
diff --git a/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/verify/onboarding-verify.tsx b/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/verify/onboarding-verify.tsx
new file mode 100644
index 00000000..ca1fe9dd
--- /dev/null
+++ b/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/verify/onboarding-verify.tsx
@@ -0,0 +1,101 @@
+'use client';
+
+import { useState } from 'react';
+import { ButtonContainer } from '@/components/button-container';
+import { LinkButton } from '@/components/ui/button';
+import { cn } from '@/utils/cn';
+import Link from 'next/link';
+
+import type {
+ IServiceCreateEventPayload,
+ IServiceProjectWithClients,
+} from '@openpanel/db';
+
+import OnboardingLayout, {
+ OnboardingDescription,
+} from '../../../onboarding-layout';
+import VerifyListener from './onboarding-verify-listener';
+
+type Props = {
+ project: IServiceProjectWithClients;
+ events: IServiceCreateEventPayload[];
+};
+
+const Verify = ({ project, events }: Props) => {
+ const [verified, setVerified] = useState(events.length > 0);
+ const client = project.clients[0];
+
+ if (!client) {
+ return Hmm, something fishy is going on. Please reload the page.
;
+ }
+
+ return (
+
+ Deploy your changes, as soon as you see events here, you're all
+ set!
+
+ }
+ >
+ {/*
+ Sadly we cant have a verify for each type since we use the same client for all different types (website, app, backend)
+
+ Pros: the user just need to keep track of one client id/secret
+ Cons: we cant verify each type individually
+
+ Might be a good idea to add a verify for each type in the future, but for now we will just have one verify for all types
+
+ {project.types.map((type) => {
+ const Component = {
+ website: VerifyWeb,
+ app: VerifyApp,
+ backend: VerifyBackend,
+ }[type];
+
+ return ;
+ })} */}
+
+
+
+ Back
+
+
+
+ {!verified && (
+
+ Skip for now
+
+ )}
+
+
+ Your dashboard
+
+
+
+
+ );
+};
+
+export default Verify;
diff --git a/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/verify/page.tsx b/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/verify/page.tsx
new file mode 100644
index 00000000..4631620f
--- /dev/null
+++ b/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/verify/page.tsx
@@ -0,0 +1,44 @@
+import { cookies } from 'next/headers';
+import { escape } from 'sqlstring';
+
+import {
+ getCurrentOrganizations,
+ getEvents,
+ getProjectWithClients,
+} from '@openpanel/db';
+
+import OnboardingVerify from './onboarding-verify';
+
+type Props = {
+ params: {
+ projectId: string;
+ };
+};
+
+const Verify = async ({ params: { projectId } }: Props) => {
+ const orgs = await getCurrentOrganizations();
+ const organizationSlug = orgs[0]?.slug;
+ if (!organizationSlug) {
+ throw new Error('No organization found');
+ }
+ const [project, events] = await Promise.all([
+ await getProjectWithClients(projectId),
+ getEvents(
+ `SELECT * FROM events WHERE project_id = ${escape(projectId)} LIMIT 100`
+ ),
+ ]);
+ const clientSecret = cookies().get('onboarding_client_secret')?.value ?? null;
+
+ if (!project) {
+ return Hmm, something fishy is going on. Please reload the page.
;
+ }
+
+ // set visible client secret from cookie
+ if (clientSecret && project.clients[0]) {
+ project.clients[0].secret = clientSecret;
+ }
+
+ return ;
+};
+
+export default Verify;
diff --git a/apps/dashboard/src/app/(onboarding)/onboarding/connect/page.tsx b/apps/dashboard/src/app/(onboarding)/onboarding/connect/page.tsx
deleted file mode 100644
index 1dacfa3c..00000000
--- a/apps/dashboard/src/app/(onboarding)/onboarding/connect/page.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import { ButtonContainer } from '@/components/button-container';
-import { Button, LinkButton } from '@/components/ui/button';
-import { Checkbox } from '@/components/ui/checkbox';
-import type { CheckboxProps } from '@radix-ui/react-checkbox';
-import Link from 'next/link';
-
-import OnboardingLayout, {
- OnboardingDescription,
-} from '../../onboarding-layout';
-
-function CheckboxGroup({
- label,
- description,
- ...props
-}: { label: string; description: string } & CheckboxProps) {
- const randId = Math.random().toString(36).substring(7);
- return (
-
- );
-}
-
-const Connect = () => {
- return (
-
- Create your account and start taking control of your data.
-
- }
- >
-
-
-
-
-
-
-
-
- Back
-
-
- Next
-
-
-
- );
-};
-
-export default Connect;
diff --git a/apps/dashboard/src/app/(onboarding)/onboarding/onboarding-tracking.tsx b/apps/dashboard/src/app/(onboarding)/onboarding/onboarding-tracking.tsx
new file mode 100644
index 00000000..d2d5edc1
--- /dev/null
+++ b/apps/dashboard/src/app/(onboarding)/onboarding/onboarding-tracking.tsx
@@ -0,0 +1,218 @@
+'use client';
+
+import { useEffect } from 'react';
+import AnimateHeight from '@/components/animate-height';
+import { ButtonContainer } from '@/components/button-container';
+import { InputWithLabel } from '@/components/forms/input-with-label';
+import { Button } from '@/components/ui/button';
+import { Switch } from '@/components/ui/switch';
+import { api, handleError } from '@/trpc/client';
+import { cn } from '@/utils/cn';
+import { zodResolver } from '@hookform/resolvers/zod';
+import type { LucideIcon } from 'lucide-react';
+import { MonitorIcon, ServerIcon, SmartphoneIcon } from 'lucide-react';
+import { useRouter } from 'next/navigation';
+import type { ControllerRenderProps, 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';
+
+function CheckboxGroup({
+ label,
+ description,
+ Icon,
+ children,
+ onChange,
+ value,
+ disabled,
+ error,
+}: {
+ label: string;
+ description: string;
+ Icon: LucideIcon;
+ children?: React.ReactNode;
+ error?: string;
+} & ControllerRenderProps) {
+ const randId = Math.random().toString(36).substring(7);
+ return (
+
+
+ {children}
+
+ );
+}
+
+type IForm = z.infer;
+
+const Tracking = () => {
+ const router = useRouter();
+ const mutation = api.onboarding.project.useMutation({
+ onError: handleError,
+ onSuccess(res) {
+ router.push(`/onboarding/${res.projectId}/connect`);
+ },
+ });
+
+ const form = useForm({
+ resolver: zodResolver(zOnboardingProject),
+ defaultValues: {
+ organization: '',
+ project: '',
+ domain: null,
+ website: false,
+ app: false,
+ backend: false,
+ },
+ });
+
+ const isWebsite = useWatch({
+ name: 'website',
+ control: form.control,
+ });
+
+ const isApp = useWatch({
+ name: 'app',
+ control: form.control,
+ });
+
+ const isBackend = useWatch({
+ name: 'backend',
+ control: form.control,
+ });
+
+ useEffect(() => {
+ if (!isWebsite) {
+ form.setValue('domain', null);
+ }
+ }, [isWebsite, form]);
+
+ const onSubmit: SubmitHandler = (values) => {
+ mutation.mutate(values);
+ };
+
+ useEffect(() => {
+ form.clearErrors();
+ }, [isWebsite, isApp, isBackend]);
+
+ return (
+
+ );
+};
+
+export default Tracking;
diff --git a/apps/dashboard/src/app/(onboarding)/onboarding/page.tsx b/apps/dashboard/src/app/(onboarding)/onboarding/page.tsx
index 3380fbc0..8acfb12c 100644
--- a/apps/dashboard/src/app/(onboarding)/onboarding/page.tsx
+++ b/apps/dashboard/src/app/(onboarding)/onboarding/page.tsx
@@ -1,76 +1,7 @@
-import { ButtonContainer } from '@/components/button-container';
-import { LinkButton } from '@/components/ui/button';
-import { Checkbox } from '@/components/ui/checkbox';
-import type { CheckboxProps } from '@radix-ui/react-checkbox';
-
-import OnboardingLayout, { OnboardingDescription } from '../onboarding-layout';
-
-function CheckboxGroup({
- label,
- description,
- ...props
-}: { label: string; description: string } & CheckboxProps) {
- const randId = Math.random().toString(36).substring(7);
- return (
-
- );
-}
+import OnboardingTracking from './onboarding-tracking';
const Tracking = () => {
- return (
-
- Create your account and start taking control of your data.
-
- }
- >
-
-
-
-
-
-
-
-
- Back
-
-
- Next
-
-
-
- );
+ return ;
};
export default Tracking;
diff --git a/apps/dashboard/src/app/(onboarding)/onboarding/verify/page.tsx b/apps/dashboard/src/app/(onboarding)/onboarding/verify/page.tsx
deleted file mode 100644
index ee2bafae..00000000
--- a/apps/dashboard/src/app/(onboarding)/onboarding/verify/page.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-import { ButtonContainer } from '@/components/button-container';
-import { Button, LinkButton } from '@/components/ui/button';
-import { Checkbox } from '@/components/ui/checkbox';
-import type { CheckboxProps } from '@radix-ui/react-checkbox';
-import Link from 'next/link';
-
-import OnboardingLayout, {
- OnboardingDescription,
-} from '../../onboarding-layout';
-
-function CheckboxGroup({
- label,
- description,
- ...props
-}: { label: string; description: string } & CheckboxProps) {
- const randId = Math.random().toString(36).substring(7);
- return (
-
- );
-}
-
-const Tracking = () => {
- return (
-
- Create your account and start taking control of your data.
-
- }
- >
-
-
-
-
-
-
-
-
- Back
-
-
- Your dashboard
-
-
-
- );
-};
-
-export default Tracking;
diff --git a/apps/dashboard/src/app/(onboarding)/steps.tsx b/apps/dashboard/src/app/(onboarding)/steps.tsx
index d964c9f3..ec973cda 100644
--- a/apps/dashboard/src/app/(onboarding)/steps.tsx
+++ b/apps/dashboard/src/app/(onboarding)/steps.tsx
@@ -1,13 +1,13 @@
'use client';
import { cn } from '@/utils/cn';
-import { ArrowRightCircleIcon, CheckCheckIcon, Edit2Icon } from 'lucide-react';
+import { CheckCheckIcon } from 'lucide-react';
import { usePathname } from 'next/navigation';
type Step = {
name: string;
status: 'completed' | 'current' | 'pending';
- href: string;
+ match: string;
};
type Props = {
@@ -15,32 +15,32 @@ type Props = {
};
function useSteps(path: string) {
- console.log('path', path);
-
const steps: Step[] = [
{
name: 'Account creation',
status: 'pending',
- href: '/get-started',
+ match: '/sign-up',
},
{
- name: 'Tracking information',
+ name: 'General',
status: 'pending',
- href: '/onboarding',
+ match: '/onboarding',
},
{
name: 'Connect your data',
status: 'pending',
- href: '/onboarding/connect',
+ match: '/onboarding/(.+)/connect',
},
{
name: 'Verify',
status: 'pending',
- href: '/onboarding/verify',
+ match: '/onboarding/(.+)/verify',
},
];
- const matchIndex = steps.findLastIndex((step) => path.startsWith(step.href));
+ const matchIndex = steps.findLastIndex((step) =>
+ path.match(new RegExp(step.match))
+ );
return steps.map((step, index) => {
if (index < matchIndex) {
@@ -87,18 +87,18 @@ const Steps = ({ className }: Props) => {
{step.status === 'current' && (
-
+
)}
{step.status === 'completed' &&
}
diff --git a/apps/dashboard/src/components/forms/input-with-label.tsx b/apps/dashboard/src/components/forms/input-with-label.tsx
index e2d91c66..da47ba3e 100644
--- a/apps/dashboard/src/components/forms/input-with-label.tsx
+++ b/apps/dashboard/src/components/forms/input-with-label.tsx
@@ -1,26 +1,40 @@
import { forwardRef } from 'react';
+import { BanIcon, InfoIcon } from 'lucide-react';
import { Input } from '../ui/input';
import type { InputProps } from '../ui/input';
import { Label } from '../ui/label';
+import { Tooltiper } from '../ui/tooltip';
type InputWithLabelProps = InputProps & {
label: string;
error?: string | undefined;
+ info?: string;
};
export const InputWithLabel = forwardRef
(
- ({ label, className, ...props }, ref) => {
+ ({ label, className, info, ...props }, ref) => {
return (
-
-