From 768e6953ac94c690479b23c73ae3efae5a916377 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 23 Oct 2025 12:03:21 -0400 Subject: [PATCH 1/2] [dev] [carhartlewis] lewis/comp-onboarding-ui (#1682) * feat(onboarding): enhance onboarding layout with sidebar and animations * refactor(onboarding): move custom value state and refs to top level --------- Co-authored-by: Lewis Carhart Co-authored-by: Mariano Fuentes --- .../app/(app)/onboarding/[orgId]/layout.tsx | 24 +- .../src/app/(app)/onboarding/[orgId]/page.tsx | 2 +- .../components/PostPaymentOnboarding.tsx | 277 +++++++++------- .../src/app/(app)/setup/[setupId]/page.tsx | 38 +-- .../setup/components/FrameworkSelection.tsx | 6 +- .../src/app/(app)/setup/components/Logo.tsx | 17 + .../components/OnboardingFormActions.tsx | 13 +- .../setup/components/OnboardingSidebar.tsx | 136 ++++++++ .../setup/components/OnboardingStepInput.tsx | 310 ++++++++++++------ .../components/OrganizationSetupForm.tsx | 132 +++----- apps/app/src/app/(app)/setup/layout.tsx | 3 +- apps/app/src/components/animated-wrapper.tsx | 64 ++++ apps/app/src/components/framework-pill.tsx | 27 ++ .../src/components/layout/MinimalHeader.tsx | 42 +-- .../onboarding/OnboardingLayout.tsx | 11 +- apps/app/src/components/selectable-pill.tsx | 51 +++ apps/app/src/hooks/usePageLoadAnimation.ts | 41 +++ 17 files changed, 810 insertions(+), 384 deletions(-) create mode 100644 apps/app/src/app/(app)/setup/components/Logo.tsx create mode 100644 apps/app/src/app/(app)/setup/components/OnboardingSidebar.tsx create mode 100644 apps/app/src/components/animated-wrapper.tsx create mode 100644 apps/app/src/components/framework-pill.tsx create mode 100644 apps/app/src/components/selectable-pill.tsx create mode 100644 apps/app/src/hooks/usePageLoadAnimation.ts diff --git a/apps/app/src/app/(app)/onboarding/[orgId]/layout.tsx b/apps/app/src/app/(app)/onboarding/[orgId]/layout.tsx index 8152247a3..c270f5c48 100644 --- a/apps/app/src/app/(app)/onboarding/[orgId]/layout.tsx +++ b/apps/app/src/app/(app)/onboarding/[orgId]/layout.tsx @@ -1,9 +1,10 @@ import { CheckoutCompleteDialog } from '@/components/dialogs/checkout-complete-dialog'; -import { OnboardingLayout } from '@/components/onboarding/OnboardingLayout'; +import { MinimalHeader } from '@/components/layout/MinimalHeader'; import { auth } from '@/utils/auth'; import { db } from '@db'; import { headers } from 'next/headers'; import { notFound } from 'next/navigation'; +import { OnboardingSidebar } from '../../setup/components/OnboardingSidebar'; interface OnboardingRouteLayoutProps { children: React.ReactNode; @@ -42,9 +43,24 @@ export default async function OnboardingRouteLayout({ } return ( - - {children} +
+
+ {/* Form Section - Left Side */} +
+ + {children} +
+ + {/* Sidebar Section - Right Side, Hidden on Mobile */} +
+ +
+
- +
); } diff --git a/apps/app/src/app/(app)/onboarding/[orgId]/page.tsx b/apps/app/src/app/(app)/onboarding/[orgId]/page.tsx index e1b081bd0..a8a5e59ce 100644 --- a/apps/app/src/app/(app)/onboarding/[orgId]/page.tsx +++ b/apps/app/src/app/(app)/onboarding/[orgId]/page.tsx @@ -77,7 +77,7 @@ export default async function OnboardingPage({ params }: OnboardingPageProps) { Object.assign(initialData, { describe: initialData.describe || - 'comp ai is a grc platform saas that gets companies compliant with soc2 iso and hipaa in days', + 'Bubba AI, Inc. is the company behind Comp AI - the fastest way to get SOC 2 compliant.', industry: initialData.industry || 'SaaS', teamSize: initialData.teamSize || '1-10', devices: initialData.devices || 'Personal laptops', diff --git a/apps/app/src/app/(app)/onboarding/components/PostPaymentOnboarding.tsx b/apps/app/src/app/(app)/onboarding/components/PostPaymentOnboarding.tsx index a5116e889..9af1b2431 100644 --- a/apps/app/src/app/(app)/onboarding/components/PostPaymentOnboarding.tsx +++ b/apps/app/src/app/(app)/onboarding/components/PostPaymentOnboarding.tsx @@ -1,13 +1,15 @@ 'use client'; import { OnboardingStepInput } from '@/app/(app)/setup/components/OnboardingStepInput'; +import { AnimatedWrapper } from '@/components/animated-wrapper'; import { LogoSpinner } from '@/components/logo-spinner'; import { Button } from '@comp/ui/button'; -import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@comp/ui/card'; import { Form, FormControl, FormField, FormItem, FormMessage } from '@comp/ui/form'; import type { Organization } from '@db'; -import { ArrowLeft, ArrowRight } from 'lucide-react'; +import { AnimatePresence, motion } from 'framer-motion'; +import { Loader2 } from 'lucide-react'; import { useEffect, useMemo } from 'react'; +import Balancer from 'react-wrap-balancer'; import { usePostPaymentOnboarding } from '../hooks/usePostPaymentOnboarding'; interface PostPaymentOnboardingProps { @@ -51,6 +53,17 @@ export function PostPaymentOnboarding({ ); }, []); + // Check if current step has valid input + const currentStepValue = form.watch(step?.key); + const isCurrentStepValid = (() => { + if (!step) return false; + if (step.key === 'frameworkIds') { + return Array.isArray(currentStepValue) && currentStepValue.length > 0; + } + // For other fields, check if they have a value + return Boolean(currentStepValue) && String(currentStepValue).trim().length > 0; + })(); + // Dispatch custom event for background animation when step changes useEffect(() => { if (typeof window !== 'undefined') { @@ -73,132 +86,166 @@ export function PostPaymentOnboarding({ } }, [stepIndex, isFinalizing, totalSteps]); - return ( -
-
- - {(isLoading || isFinalizing) && ( -
- -
- )} - -
- -
- Step {stepIndex + 1} of {totalSteps} -
- - {step?.question || ''} - -
-
- - {!isLoading && ( -
+ return isFinalizing ? ( +
+ +
+ ) : ( +
+ {/* Progress Stepper */} + +
+
+
+ + + {/* Main Content */} +
+ {/* Title */} +
+ +

+ {step?.question || ''} +

+
+ +

+ Our AI will personalize the platform based on your answers. +

+
+
+ + {/* Form Content */} +
+ {!isLoading && step && ( + + - {steps.map((s, idx) => ( -
- ( - - - - - {s.key !== 'shipping' && ( -
- -
- )} -
- )} - /> -
- ))} + ( + + + + +
+ +
+
+ )} + /> +
+ )} +
+ + {/* Action Buttons - Fixed at bottom */} +
+ + {stepIndex > 0 && ( + + + )} - - -
+ + {isLocal && ( + - -
- {isLocal && ( - - )} - -
-
-
-

- - - - - - AI personalizes your plan based on your answers - - -

-
-
- + {isOnboarding && } + Complete + + + ) : ( + + )} + +
); diff --git a/apps/app/src/app/(app)/setup/[setupId]/page.tsx b/apps/app/src/app/(app)/setup/[setupId]/page.tsx index 13ade6e4d..16e4bac81 100644 --- a/apps/app/src/app/(app)/setup/[setupId]/page.tsx +++ b/apps/app/src/app/(app)/setup/[setupId]/page.tsx @@ -1,9 +1,9 @@ -import { getOrganizations } from '@/data/getOrganizations'; +import { MinimalHeader } from '@/components/layout/MinimalHeader'; import { auth } from '@/utils/auth'; -import type { Organization } from '@db'; import { Metadata } from 'next'; import { headers } from 'next/headers'; import { redirect } from 'next/navigation'; +import { OnboardingSidebar } from '../components/OnboardingSidebar'; import { OrganizationSetupForm } from '../components/OrganizationSetupForm'; import { getSetupSession } from '../lib/setup-session'; @@ -41,23 +41,23 @@ export default async function SetupWithIdPage({ params, searchParams }: SetupPag return redirect(`/invite/${inviteCode}`); } - // Fetch existing organizations - let organizations: Organization[] = []; - - try { - const result = await getOrganizations(); - organizations = result.organizations; - } catch (error) { - // If user has no organizations, continue with empty array - console.error('Failed to fetch organizations:', error); - } - return ( - +
+ {/* Form Section - Left Side */} +
+ + + +
+ + {/* Sidebar Section - Right Side, Hidden on Mobile */} +
+ +
+
); } diff --git a/apps/app/src/app/(app)/setup/components/FrameworkSelection.tsx b/apps/app/src/app/(app)/setup/components/FrameworkSelection.tsx index 27fbd5ca4..28287c9a3 100644 --- a/apps/app/src/app/(app)/setup/components/FrameworkSelection.tsx +++ b/apps/app/src/app/(app)/setup/components/FrameworkSelection.tsx @@ -1,6 +1,6 @@ 'use client'; -import { FrameworkCard } from '@/components/framework-card'; +import { FrameworkPill } from '@/components/framework-pill'; import type { FrameworkEditorFramework } from '@db'; import { useEffect, useRef, useState } from 'react'; @@ -58,11 +58,11 @@ export function FrameworkSelection({ value, onChange, onLoadingChange }: Framewo } return ( -
+
{frameworks .filter((framework) => framework.visible) .map((framework) => ( - ) => ( + + + +); diff --git a/apps/app/src/app/(app)/setup/components/OnboardingFormActions.tsx b/apps/app/src/app/(app)/setup/components/OnboardingFormActions.tsx index 33c605ca3..1bd5361c2 100644 --- a/apps/app/src/app/(app)/setup/components/OnboardingFormActions.tsx +++ b/apps/app/src/app/(app)/setup/components/OnboardingFormActions.tsx @@ -1,6 +1,6 @@ import { Button } from '@comp/ui/button'; import { AnimatePresence, motion } from 'framer-motion'; -import { ArrowLeft, ArrowRight, Loader2 } from 'lucide-react'; +import { Loader2 } from 'lucide-react'; interface OnboardingFormActionsProps { onBack: () => void; @@ -32,13 +32,12 @@ export function OnboardingFormActions({ > )} @@ -67,8 +66,7 @@ export function OnboardingFormActions({ className="flex items-center gap-2" > {isOnboarding && } - Finish - + Complete ) : ( @@ -87,8 +85,7 @@ export function OnboardingFormActions({ transition={{ duration: 0.2 }} className="flex items-center" > - Next - + Continue )} diff --git a/apps/app/src/app/(app)/setup/components/OnboardingSidebar.tsx b/apps/app/src/app/(app)/setup/components/OnboardingSidebar.tsx new file mode 100644 index 000000000..3ec78ef1e --- /dev/null +++ b/apps/app/src/app/(app)/setup/components/OnboardingSidebar.tsx @@ -0,0 +1,136 @@ +import { SVGProps } from 'react'; + +export const OnboardingSidebar = (props: SVGProps) => ( + + + + + + + + + + + + + + + + + + + + + +); diff --git a/apps/app/src/app/(app)/setup/components/OnboardingStepInput.tsx b/apps/app/src/app/(app)/setup/components/OnboardingStepInput.tsx index f0f38a230..4d3505048 100644 --- a/apps/app/src/app/(app)/setup/components/OnboardingStepInput.tsx +++ b/apps/app/src/app/(app)/setup/components/OnboardingStepInput.tsx @@ -1,23 +1,23 @@ +import { AnimatedWrapper } from '@/components/animated-wrapper'; +import { SelectablePill } from '@/components/selectable-pill'; import { FormLabel } from '@comp/ui/form'; import { Input } from '@comp/ui/input'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@comp/ui/select'; -import { SelectPills } from '@comp/ui/select-pills'; import { Textarea } from '@comp/ui/textarea'; +import { X } from 'lucide-react'; +import { useRef, useState } from 'react'; import type { UseFormReturn } from 'react-hook-form'; import { Controller } from 'react-hook-form'; import type { CompanyDetails, Step } from '../lib/types'; import { FrameworkSelection } from './FrameworkSelection'; import { WebsiteInput } from './WebsiteInput'; -// Type for form fields used in this component. -// For now, defining it here to match OrganizationSetupForm.tsx structure. export type OnboardingFormFields = Partial & { [K in keyof CompanyDetails as `${K}Other`]?: string; }; interface OnboardingStepInputProps { currentStep: Step; - form: UseFormReturn; // Or a more generic form type if preferred + form: UseFormReturn; savedAnswers: Partial; onLoadingChange?: (loading: boolean) => void; } @@ -28,153 +28,251 @@ export function OnboardingStepInput({ savedAnswers, onLoadingChange, }: OnboardingStepInputProps) { + // Hooks must be called at the top level + const [customValue, setCustomValue] = useState(''); + const inputRef = useRef(null); + const containerRef = useRef(null); + if (currentStep.key === 'frameworkIds') { return ( -
- form.setValue(currentStep.key, value)} - onLoadingChange={onLoadingChange} - /> -
+ +
+ form.setValue(currentStep.key, value)} + onLoadingChange={onLoadingChange} + /> +
+
); } if (currentStep.key === 'shipping') { return (
-
-
- Full Name - -

- {form.formState.errors.shipping?.fullName?.message} -

+ +
+
+ Full Name + +

+ {form.formState.errors.shipping?.fullName?.message} +

+
+
+ Phone + +

+ {form.formState.errors.shipping?.phone?.message} +

+
-
- Phone - + +
+ Address +