diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/page.tsx
index 2ed6f5fb..26a449a9 100644
--- a/apps/dashboard/src/app/(app)/[organizationSlug]/page.tsx
+++ b/apps/dashboard/src/app/(app)/[organizationSlug]/page.tsx
@@ -1,5 +1,6 @@
import { LogoSquare } from '@/components/logo';
import { ProjectCard } from '@/components/projects/project-card';
+import { SignOutButton } from '@clerk/nextjs';
import { notFound, redirect } from 'next/navigation';
import {
@@ -62,6 +63,7 @@ export default async function Page({
return (
+
Select project
{projects.map((item) => (
diff --git a/apps/dashboard/src/app/(onboarding)/get-started/get-started.tsx b/apps/dashboard/src/app/(onboarding)/get-started/get-started.tsx
new file mode 100644
index 00000000..a29dc96a
--- /dev/null
+++ b/apps/dashboard/src/app/(onboarding)/get-started/get-started.tsx
@@ -0,0 +1,97 @@
+'use client';
+
+import { Button } from '@/components/ui/button';
+import { SignInButton, useSignIn, useSignUp } from '@clerk/nextjs';
+import type { OAuthStrategy } from '@clerk/nextjs/dist/types/server';
+import { toast } from 'sonner';
+
+import OnboardingLayout, { OnboardingDescription } from '../onboarding-layout';
+
+const GetStarted = () => {
+ const { signIn } = useSignIn();
+
+ const signInWith = (strategy: OAuthStrategy) => {
+ if (!signIn) {
+ return toast.error('Sign in is not available at the moment');
+ }
+ return signIn.authenticateWithRedirect({
+ strategy,
+ redirectUrl: '/sso-callback',
+ redirectUrlComplete: '/onboarding',
+ });
+ };
+
+ return (
+
+ Create your account and start taking control of your data.
+
+ }
+ >
+
+
+
+
+
+ Already have an account?{' '}
+
+ Sign in
+
+
+
+ );
+};
+
+export default GetStarted;
diff --git a/apps/dashboard/src/app/(onboarding)/get-started/page.tsx b/apps/dashboard/src/app/(onboarding)/get-started/page.tsx
new file mode 100644
index 00000000..26d8e651
--- /dev/null
+++ b/apps/dashboard/src/app/(onboarding)/get-started/page.tsx
@@ -0,0 +1,15 @@
+import { auth } from '@clerk/nextjs';
+import { redirect } from 'next/navigation';
+
+import GetStartedClient from './get-started';
+
+// Sign up
+const GetStarted = () => {
+ const session = auth();
+ if (session.userId) {
+ return redirect('/');
+ }
+ return
;
+};
+
+export default GetStarted;
diff --git a/apps/dashboard/src/app/(onboarding)/layout.tsx b/apps/dashboard/src/app/(onboarding)/layout.tsx
new file mode 100644
index 00000000..9d8bcd86
--- /dev/null
+++ b/apps/dashboard/src/app/(onboarding)/layout.tsx
@@ -0,0 +1,43 @@
+import { Logo } from '@/components/logo';
+
+import SkipOnboarding from './skip-onboarding';
+import Steps from './steps';
+
+type Props = {
+ children: React.ReactNode;
+};
+
+const Page = ({ children }: Props) => {
+ return (
+ <>
+
+
+
+
+
+
+
+
+ Welcome to Openpanel
+
+
Get started
+
+
+
+
{children}
+
+
+
+ >
+ );
+};
+
+export default Page;
diff --git a/apps/dashboard/src/app/(onboarding)/onboarding-layout.tsx b/apps/dashboard/src/app/(onboarding)/onboarding-layout.tsx
new file mode 100644
index 00000000..0257e364
--- /dev/null
+++ b/apps/dashboard/src/app/(onboarding)/onboarding-layout.tsx
@@ -0,0 +1,37 @@
+import { cn } from '@/utils/cn';
+
+type Props = {
+ children: React.ReactNode;
+ className?: string;
+ title: string;
+ description?: React.ReactNode;
+};
+
+export const OnboardingDescription = ({
+ children,
+ className,
+}: Pick
) => (
+
+ {children}
+
+);
+
+const OnboardingLayout = ({
+ title,
+ description,
+ children,
+ className,
+}: Props) => {
+ return (
+
+
+
{title}
+ {description}
+
+
+ {children}
+
+ );
+};
+
+export default OnboardingLayout;
diff --git a/apps/dashboard/src/app/(onboarding)/onboarding/connect/page.tsx b/apps/dashboard/src/app/(onboarding)/onboarding/connect/page.tsx
new file mode 100644
index 00000000..1dacfa3c
--- /dev/null
+++ b/apps/dashboard/src/app/(onboarding)/onboarding/connect/page.tsx
@@ -0,0 +1,79 @@
+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/page.tsx b/apps/dashboard/src/app/(onboarding)/onboarding/page.tsx
new file mode 100644
index 00000000..3380fbc0
--- /dev/null
+++ b/apps/dashboard/src/app/(onboarding)/onboarding/page.tsx
@@ -0,0 +1,76 @@
+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 (
+
+ );
+}
+
+const Tracking = () => {
+ return (
+
+ Create your account and start taking control of your data.
+
+ }
+ >
+
+
+
+
+
+
+
+
+ Back
+
+
+ Next
+
+
+
+ );
+};
+
+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
new file mode 100644
index 00000000..ee2bafae
--- /dev/null
+++ b/apps/dashboard/src/app/(onboarding)/onboarding/verify/page.tsx
@@ -0,0 +1,75 @@
+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)/skip-onboarding.tsx b/apps/dashboard/src/app/(onboarding)/skip-onboarding.tsx
new file mode 100644
index 00000000..2df5162e
--- /dev/null
+++ b/apps/dashboard/src/app/(onboarding)/skip-onboarding.tsx
@@ -0,0 +1,21 @@
+'use client';
+
+import { ChevronLastIcon } from 'lucide-react';
+import Link from 'next/link';
+import { usePathname } from 'next/navigation';
+
+const SkipOnboarding = () => {
+ const pathname = usePathname();
+ if (!pathname.startsWith('/onboarding')) return null;
+ return (
+
+ Skip onboarding
+
+
+ );
+};
+
+export default SkipOnboarding;
diff --git a/apps/dashboard/src/app/(onboarding)/sso-callback/page.tsx b/apps/dashboard/src/app/(onboarding)/sso-callback/page.tsx
new file mode 100644
index 00000000..7d0819e8
--- /dev/null
+++ b/apps/dashboard/src/app/(onboarding)/sso-callback/page.tsx
@@ -0,0 +1,7 @@
+import { AuthenticateWithRedirectCallback } from '@clerk/nextjs';
+
+const SSOCallback = () => {
+ return ;
+};
+
+export default SSOCallback;
diff --git a/apps/dashboard/src/app/(onboarding)/steps.tsx b/apps/dashboard/src/app/(onboarding)/steps.tsx
new file mode 100644
index 00000000..d964c9f3
--- /dev/null
+++ b/apps/dashboard/src/app/(onboarding)/steps.tsx
@@ -0,0 +1,122 @@
+'use client';
+
+import { cn } from '@/utils/cn';
+import { ArrowRightCircleIcon, CheckCheckIcon, Edit2Icon } from 'lucide-react';
+import { usePathname } from 'next/navigation';
+
+type Step = {
+ name: string;
+ status: 'completed' | 'current' | 'pending';
+ href: string;
+};
+
+type Props = {
+ className?: string;
+};
+
+function useSteps(path: string) {
+ console.log('path', path);
+
+ const steps: Step[] = [
+ {
+ name: 'Account creation',
+ status: 'pending',
+ href: '/get-started',
+ },
+ {
+ name: 'Tracking information',
+ status: 'pending',
+ href: '/onboarding',
+ },
+ {
+ name: 'Connect your data',
+ status: 'pending',
+ href: '/onboarding/connect',
+ },
+ {
+ name: 'Verify',
+ status: 'pending',
+ href: '/onboarding/verify',
+ },
+ ];
+
+ const matchIndex = steps.findLastIndex((step) => path.startsWith(step.href));
+
+ return steps.map((step, index) => {
+ if (index < matchIndex) {
+ return { ...step, status: 'completed' };
+ }
+ if (index === matchIndex) {
+ return { ...step, status: 'current' };
+ }
+ return step;
+ });
+}
+
+const Steps = ({ className }: Props) => {
+ const path = usePathname();
+ const steps = useSteps(path);
+ const currentIndex = steps.findIndex((i) => i.status === 'current');
+ return (
+
+
+
+
+ {steps.map((step, index) => (
+
+
+
+ {step.status === 'current' && (
+
+ )}
+
+ {step.status === 'completed' &&
}
+ {/* {step.status === 'current' && (
+
+ )} */}
+ {(step.status === 'pending' || step.status === 'current') && (
+ <>{index + 1}>
+ )}
+
+
+
+
{step.name}
+
+ ))}
+
+
+ );
+};
+
+export default Steps;
diff --git a/apps/dashboard/src/components/ui/button.tsx b/apps/dashboard/src/components/ui/button.tsx
index 1df7e998..046da5c1 100644
--- a/apps/dashboard/src/components/ui/button.tsx
+++ b/apps/dashboard/src/components/ui/button.tsx
@@ -7,6 +7,7 @@ import { cva } from 'class-variance-authority';
import type { VariantProps } from 'class-variance-authority';
import type { LucideIcon } from 'lucide-react';
import { Loader2 } from 'lucide-react';
+import Link from 'next/link';
const buttonVariants = cva(
'inline-flex flex-shrink-0 items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
@@ -94,5 +95,61 @@ Button.displayName = 'Button';
Button.defaultProps = {
type: 'button',
};
+export interface LinkButtonProps
+ extends React.AnchorHTMLAttributes,
+ VariantProps {
+ asChild?: boolean;
+ loading?: boolean;
+ icon?: LucideIcon;
+ responsive?: boolean;
+ href: string;
+}
-export { Button, buttonVariants };
+const LinkButton = React.forwardRef<
+ typeof Link,
+ React.PropsWithoutRef
+>(
+ (
+ {
+ className,
+ variant,
+ size,
+ children,
+ loading,
+ icon,
+ responsive,
+ href,
+ ...props
+ },
+ ref
+ ) => {
+ const Icon = loading ? Loader2 : icon ?? null;
+ return (
+
+ {Icon && (
+
+ )}
+ {responsive ? (
+ {children}
+ ) : (
+ children
+ )}
+
+ );
+ }
+);
+LinkButton.displayName = 'LinkButton';
+
+export { Button, LinkButton, buttonVariants };
diff --git a/apps/dashboard/src/middleware.ts b/apps/dashboard/src/middleware.ts
index 1ae838c9..c109fe0c 100644
--- a/apps/dashboard/src/middleware.ts
+++ b/apps/dashboard/src/middleware.ts
@@ -4,7 +4,10 @@ import { authMiddleware } from '@clerk/nextjs';
// Please edit this to allow other routes to be public as needed.
// See https://clerk.com/docs/references/nextjs/auth-middleware for more information about configuring your Middleware
export default authMiddleware({
+ debug: true,
+ signInUrl: '/get-started',
publicRoutes: [
+ '/get-started(.*)',
'/share/overview/:id',
'/api/trpc(.*)',
'/api/clerk/(.*)?',
diff --git a/apps/dashboard/tailwind.config.js b/apps/dashboard/tailwind.config.js
index 603f31ee..223b3199 100644
--- a/apps/dashboard/tailwind.config.js
+++ b/apps/dashboard/tailwind.config.js
@@ -139,10 +139,19 @@ const config = {
from: { height: 'var(--radix-accordion-content-height)' },
to: { height: '0px' },
},
+ wiggle: {
+ '0%': { transform: 'rotate(0deg)' },
+ '80%': { transform: 'rotate(0deg)' },
+ '85%': { transform: 'rotate(5deg)' },
+ '95%': { transform: 'rotate(-5deg)' },
+ '100%': { transform: 'rotate(0deg)' },
+ },
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
+ wiggle: 'wiggle 2.5s ease-in-out infinite',
+ 'ping-slow': 'ping 1.5s ease-in-out infinite',
},
},
},