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', }, }, },