From 4d7ccae0e5edfb7feed374651fe2d642ac5d1623 Mon Sep 17 00:00:00 2001 From: icld Date: Tue, 25 Feb 2025 16:17:28 -0800 Subject: [PATCH 1/3] Build onboarding from docs comments --- packages/frontend/src/App.tsx | 14 +- .../onboarding/NavigationSidebar.tsx | 110 +++++++++++ .../onboarding/OnboardingDialog.tsx | 35 ++++ .../onboarding/OnboardingLayout.tsx | 88 +++++++++ .../onboarding/ProgressIndicator.tsx | 60 ++++++ .../src/components/onboarding/README.md | 31 ++++ .../onboarding/docs/Component-Breakdown.md | 88 +++++++++ .../onboarding/docs/Form-Integration-Plan.md | 133 +++++++++++++ .../onboarding/docs/State-Management.md | 124 +++++++++++++ .../src/components/onboarding/index.ts | 24 +++ .../onboarding/store/navigationStore.ts | 175 ++++++++++++++++++ .../onboarding/views/ConfigureView.tsx | 163 ++++++++++++++++ .../onboarding/views/ConnectView.tsx | 143 ++++++++++++++ .../onboarding/views/DeployView.tsx | 126 +++++++++++++ .../views/DeploymentOptionsView.tsx | 151 +++++++++++++++ .../onboarding/views/RepositoryListView.tsx | 106 +++++++++++ .../views/TemplateSelectionView.tsx | 146 +++++++++++++++ packages/frontend/src/pages/AuthPage.tsx | 14 +- .../frontend/src/pages/OnboardingDemoPage.tsx | 57 ++++++ .../frontend/src/pages/OnboardingPage.tsx | 15 ++ 20 files changed, 1801 insertions(+), 2 deletions(-) create mode 100644 packages/frontend/src/components/onboarding/NavigationSidebar.tsx create mode 100644 packages/frontend/src/components/onboarding/OnboardingDialog.tsx create mode 100644 packages/frontend/src/components/onboarding/OnboardingLayout.tsx create mode 100644 packages/frontend/src/components/onboarding/ProgressIndicator.tsx create mode 100644 packages/frontend/src/components/onboarding/README.md create mode 100644 packages/frontend/src/components/onboarding/docs/Component-Breakdown.md create mode 100644 packages/frontend/src/components/onboarding/docs/Form-Integration-Plan.md create mode 100644 packages/frontend/src/components/onboarding/docs/State-Management.md create mode 100644 packages/frontend/src/components/onboarding/index.ts create mode 100644 packages/frontend/src/components/onboarding/store/navigationStore.ts create mode 100644 packages/frontend/src/components/onboarding/views/ConfigureView.tsx create mode 100644 packages/frontend/src/components/onboarding/views/ConnectView.tsx create mode 100644 packages/frontend/src/components/onboarding/views/DeployView.tsx create mode 100644 packages/frontend/src/components/onboarding/views/DeploymentOptionsView.tsx create mode 100644 packages/frontend/src/components/onboarding/views/RepositoryListView.tsx create mode 100644 packages/frontend/src/components/onboarding/views/TemplateSelectionView.tsx create mode 100644 packages/frontend/src/pages/OnboardingDemoPage.tsx create mode 100644 packages/frontend/src/pages/OnboardingPage.tsx diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index 1f8edbea..33ec9cb0 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -5,6 +5,8 @@ import { DashboardLayout } from './layouts/DashboardLayout'; import Index from './pages'; import AuthPage from './pages/AuthPage'; import BuyPrepaidService from './pages/BuyPrepaidService'; +import OnboardingDemoPage from './pages/OnboardingDemoPage'; +import OnboardingPage from './pages/OnboardingPage'; import { projectsRoutesWithoutSearch } from './pages/org-slug/projects/project-routes'; import Settings from './pages/org-slug/Settings'; import { BASE_URL } from './utils/constants'; @@ -49,6 +51,14 @@ const router = createBrowserRouter([ path: '/buy-prepaid-service', element: , }, + { + path: '/onboarding', + element: , + }, + { + path: '/onboarding-demo', + element: , + }, ]); /** @@ -64,9 +74,11 @@ function App() { credentials: 'include', }).then((res) => { const path = window.location.pathname; + const publicPaths = ['/login', '/onboarding', '/onboarding-demo']; + if (res.status !== 200) { localStorage.clear(); - if (path !== '/login') { + if (!publicPaths.includes(path)) { window.location.pathname = '/login'; } } else { diff --git a/packages/frontend/src/components/onboarding/NavigationSidebar.tsx b/packages/frontend/src/components/onboarding/NavigationSidebar.tsx new file mode 100644 index 00000000..523141bc --- /dev/null +++ b/packages/frontend/src/components/onboarding/NavigationSidebar.tsx @@ -0,0 +1,110 @@ +/** + * NavigationSidebar.tsx + * + * This component displays a sidebar with navigation steps for the onboarding flow. + * It highlights the current step and allows navigation to completed steps. + * + * Implementation: + * 1. Display the logo at the top + * 2. Show a list of navigation items for each step + * 3. Highlight the current step + * 4. Make completed steps clickable for navigation + * 5. Style according to the shadcn/ui design system + */ + +import { cn } from '@/lib/utils'; +import { + Code, + GitBranch, + Github, + Rocket, + Server, + Settings +} from 'lucide-react'; +import React from 'react'; +import useNavigationStore, { OnboardingStep } from './store/navigationStore'; + +// Map steps to icons and labels +const stepConfig = { + [OnboardingStep.CONNECT]: { + icon: Github, + label: 'Connect' + }, + [OnboardingStep.REPOSITORY]: { + icon: GitBranch, + label: 'Repository' + }, + [OnboardingStep.TEMPLATE]: { + icon: Code, + label: 'Template' + }, + [OnboardingStep.CONFIGURE]: { + icon: Settings, + label: 'Configure' + }, + [OnboardingStep.DEPLOYMENT_OPTIONS]: { + icon: Server, + label: 'Options' + }, + [OnboardingStep.DEPLOY]: { + icon: Rocket, + label: 'Deploy' + }, +}; + +const NavigationSidebar: React.FC = () => { + const { currentStep, completedSteps, setCurrentStep, canGoToStep } = useNavigationStore(); + + // Order of steps to display + const steps = [ + OnboardingStep.CONNECT, + OnboardingStep.REPOSITORY, + OnboardingStep.TEMPLATE, + OnboardingStep.CONFIGURE, + OnboardingStep.DEPLOYMENT_OPTIONS, + OnboardingStep.DEPLOY, + ]; + + return ( +
+ {/* Logo */} +
+

Snowball

+
+ + {/* Navigation Items */} + +
+ ); +}; + +export default NavigationSidebar; \ No newline at end of file diff --git a/packages/frontend/src/components/onboarding/OnboardingDialog.tsx b/packages/frontend/src/components/onboarding/OnboardingDialog.tsx new file mode 100644 index 00000000..2b4238a2 --- /dev/null +++ b/packages/frontend/src/components/onboarding/OnboardingDialog.tsx @@ -0,0 +1,35 @@ +import { + Dialog, + DialogContent, DialogTrigger +} from '@/components/ui/dialog'; +import React from 'react'; +import OnboardingLayout from './OnboardingLayout'; + +interface OnboardingDialogProps { + trigger?: React.ReactNode; + defaultOpen?: boolean; +} + +/** + * OnboardingDialog component + * + * A dialog modal that contains the onboarding flow. + * Can be triggered by a custom element or automatically opened. + */ +const OnboardingDialog: React.FC = ({ + trigger, + defaultOpen = false +}) => { + return ( + + {trigger && {trigger}} + +
+ +
+
+
+ ); +}; + +export default OnboardingDialog; \ No newline at end of file diff --git a/packages/frontend/src/components/onboarding/OnboardingLayout.tsx b/packages/frontend/src/components/onboarding/OnboardingLayout.tsx new file mode 100644 index 00000000..12d40af6 --- /dev/null +++ b/packages/frontend/src/components/onboarding/OnboardingLayout.tsx @@ -0,0 +1,88 @@ +/** + * OnboardingLayout.tsx + * + * This component serves as the main wrapper for the entire onboarding flow. + * It handles the layout structure with a sidebar and main content area. + * Modified to work well in both full page and dialog contexts. + * + * Implementation: + * 1. Use the existing layout components where possible + * 2. Render the NavigationSidebar on the left + * 3. Render the current step component in the main content area + * 4. The current step should be determined by the navigation store + * 5. Add proper spacing and responsive behavior + * + * Dependencies: + * - NavigationSidebar + * - ConnectView + * - RepositoryListView + * - TemplateSelectionView + * - ConfigureView + * - DeploymentOptionsView + * - DeployView + * - useNavigationStore + */ + +import React from 'react'; +import NavigationSidebar from './NavigationSidebar'; +import ProgressIndicator from './ProgressIndicator'; +import useNavigationStore, { OnboardingStep } from './store/navigationStore'; + +// Import view components +import ConfigureView from './views/ConfigureView'; +import ConnectView from './views/ConnectView'; +import DeploymentOptionsView from './views/DeploymentOptionsView'; +import DeployView from './views/DeployView'; +import RepositoryListView from './views/RepositoryListView'; +import TemplateSelectionView from './views/TemplateSelectionView'; + +interface OnboardingLayoutProps { + isDialog?: boolean; +} + +const OnboardingLayout: React.FC = ({ + isDialog = false +}) => { + const { currentStep } = useNavigationStore(); + + // Render the appropriate view component based on the current step + const renderCurrentView = () => { + switch (currentStep) { + case OnboardingStep.CONNECT: + return ; + case OnboardingStep.REPOSITORY: + return ; + case OnboardingStep.TEMPLATE: + return ; + case OnboardingStep.CONFIGURE: + return ; + case OnboardingStep.DEPLOYMENT_OPTIONS: + return ; + case OnboardingStep.DEPLOY: + return ; + default: + return ; + } + }; + + return ( +
+ {/* Navigation Sidebar */} + + + {/* Main Content Area */} +
+
+ {renderCurrentView()} +
+ + {/* Progress Indicator Footer */} +
+ +
+
+
+ ); +}; + +export default OnboardingLayout; \ No newline at end of file diff --git a/packages/frontend/src/components/onboarding/ProgressIndicator.tsx b/packages/frontend/src/components/onboarding/ProgressIndicator.tsx new file mode 100644 index 00000000..fa4befdd --- /dev/null +++ b/packages/frontend/src/components/onboarding/ProgressIndicator.tsx @@ -0,0 +1,60 @@ +/** + * ProgressIndicator.tsx + * + * This component displays a horizontal indicator showing the current progress + * through the onboarding flow steps. + * + * Implementation: + * 1. Use dots to represent each step + * 2. Highlight the current step + * 3. Show completed steps differently + * 4. Style according to the shadcn/ui design system + */ + +import { cn } from '@/lib/utils'; +import React from 'react'; +import useNavigationStore, { OnboardingStep } from './store/navigationStore'; + +const ProgressIndicator: React.FC = () => { + const { currentStep, completedSteps } = useNavigationStore(); + + // Define the steps to show in the progress indicator + const steps = [ + OnboardingStep.CONNECT, + OnboardingStep.REPOSITORY, + OnboardingStep.TEMPLATE, + OnboardingStep.CONFIGURE, + OnboardingStep.DEPLOYMENT_OPTIONS, + OnboardingStep.DEPLOY, + ]; + + return ( +
+ {steps.map((step, index) => { + const isActive = currentStep === step; + const isCompleted = completedSteps[step]; + + return ( +
+
+ {index < steps.length - 1 && ( +
+ )} +
+ ); + })} +
+ ); +}; + +export default ProgressIndicator; \ No newline at end of file diff --git a/packages/frontend/src/components/onboarding/README.md b/packages/frontend/src/components/onboarding/README.md new file mode 100644 index 00000000..00dc262b --- /dev/null +++ b/packages/frontend/src/components/onboarding/README.md @@ -0,0 +1,31 @@ +# Onboarding Flow + +This directory contains the implementation of the onboarding flow for the application. + +## Documentation + +Please refer to the following documentation for details about the onboarding flow: + +- [Component Breakdown](./docs/Component-Breakdown.md) - Overview of all components used in the onboarding flow +- [State Management](./docs/State-Management.md) - Details about Zustand store implementations for state management +- [Form Integration Plan](./docs/Form-Integration-Plan.md) - Guidelines for integrating existing form components + +## Implementation Approach + +The onboarding flow uses a navigation-only state management approach with Zustand to coordinate between steps, while preserving the functionality of existing form components. + +## Directory Structure + +- `/docs` - Documentation files +- `/store` - Zustand store implementation +- `/views` - Main view components for each step of the flow + +## Getting Started + +To implement the onboarding flow, follow these steps: + +1. Review the documentation in the `/docs` directory +2. Implement the navigation store as described in the State Management document +3. Create the core layout components +4. Implement each view component according to the Form Integration Plan +5. Test the flow with existing form components \ No newline at end of file diff --git a/packages/frontend/src/components/onboarding/docs/Component-Breakdown.md b/packages/frontend/src/components/onboarding/docs/Component-Breakdown.md new file mode 100644 index 00000000..67786de6 --- /dev/null +++ b/packages/frontend/src/components/onboarding/docs/Component-Breakdown.md @@ -0,0 +1,88 @@ +# Onboarding Flow Component Architecture + +## Core Layout Components + +1. **AppLayout** + - Uses shadcn/ui `Sheet` for responsive sidebar + - Main content area with proper padding + +2. **Sidebar** + - Uses shadcn/ui `Sidebar` component + - Contains logo and navigation steps + +3. **NavigationStep** + - Modified shadcn/ui `NavigationMenuItem` + - Displays icon, step label, and active state + +4. **ProgressIndicator** + - Custom component using shadcn/ui styling conventions + - Horizontal dots showing current flow progress + +## Page Components + +1. **ConnectView** + - Main component for initial GitHub connection screen + - Contains options to import or start from template + +2. **RepositoryListView** + - Displays recent repositories with timestamps + - Uses shadcn/ui `ScrollArea` for scrollable list + +3. **ConfigureView** + - Settings configuration screen with form elements + - Multiple sections for different configuration options + +4. **DeployView** + - Final deployment screen with status information + - Confirmation elements and action buttons + +## UI Components + +1. **TemplateCard** + - Uses shadcn/ui `Card`, `CardHeader`, `CardContent`, `CardFooter` + - Displays template options with icons and descriptions + +2. **ActionButton** + - Extends shadcn/ui `Button` with variants for primary/secondary actions + - Consistent styling across all screens + +3. **RepositoryItem** + - Custom list item with GitHub repo information + - Uses Lucide icons and formatting for timestamps + +4. **FormComponents** + - Uses shadcn/ui `Form`, `FormField`, `FormItem`, `FormLabel`, `FormControl` + - Includes `Input`, `Select`, `Checkbox`, etc. + +5. **EnvironmentVariablesSection** + - Form section for environment variables + - Uses shadcn/ui `Input` and add button functionality + +6. **DeploymentStatusIndicator** + - Shows deployment status with appropriate icon + - Uses shadcn/ui styling conventions + +7. **NavigationControls** + - Contains Previous/Next buttons + - Uses shadcn/ui `Button` with appropriate variants + +## Icons + +- All icons from Lucide React library: + - `Git`, `Github`, `Settings`, `Box`, `Terminal` + - `Plus`, `Check`, `ChevronRight`, `ChevronLeft` + - `ArrowRight`, `ExternalLink` + +## Form Elements + +1. **ProjectForm** + - Uses shadcn/ui `Form` component + - Fields for project name, deployment type, etc. + +2. **DropdownSelects** + - Uses shadcn/ui `Select`, `SelectTrigger`, `SelectValue`, `SelectContent`, `SelectItem` + - For container URLs, deployment numbers, etc. + +3. **CheckboxGroup** + - Group of shadcn/ui `Checkbox` components + - For environment type selection (Production, Preview, Development) \ No newline at end of file diff --git a/packages/frontend/src/components/onboarding/docs/Form-Integration-Plan.md b/packages/frontend/src/components/onboarding/docs/Form-Integration-Plan.md new file mode 100644 index 00000000..0002d0ea --- /dev/null +++ b/packages/frontend/src/components/onboarding/docs/Form-Integration-Plan.md @@ -0,0 +1,133 @@ +# Form Integration Plan for Onboarding Flow + +## Overview +This document outlines how to integrate existing form components into the onboarding flow without modifying their functionality. The navigation state will be managed by Zustand as defined previously, while the form sections will maintain their current implementation. + +## Key Form Sections to Integrate + +### Connect Step Forms +1. **GitHub Connection Form** + - Location: Appears in the first screen + - Purpose: Authenticates with GitHub + - Integration point: `ConnectView.tsx` + - Implementation note: Import existing form as-is + +2. **Repository Selection List** + - Location: Appears after GitHub connection + - Purpose: Displays repositories with timestamps for selection + - Integration point: `ConnectView.tsx` (conditional render after connection) + - Implementation note: Preserve existing event handlers + +3. **Template Selection Grid** + - Location: Alternative to repository selection + - Purpose: Displays template options + - Integration point: `ConnectView.tsx` (conditional render) + - Implementation note: Maintain existing selection logic + +### Configure Step Forms +1. **Deployment URL Configuration** + - Location: Top of configure screen + - Purpose: Sets deployment URL + - Integration point: `ConfigureView.tsx` + - Implementation note: Import without modifying validation + +2. **Environment Variables Form** + - Location: Middle of configure screen + - Purpose: Configure environment settings + - Integration point: `ConfigureView.tsx` + - Implementation note: Preserve existing add/remove functionality + +3. **Deployment Options Form** + - Location: Bottom of configure screen + - Purpose: Sets deployment constraints (max price, instances) + - Integration point: `ConfigureView.tsx` + - Implementation note: Keep all validation rules intact + +4. **Environment Type Selection** + - Location: Configure screen + - Purpose: Select environments (Production/Preview/Development) + - Integration point: `ConfigureView.tsx` + - Implementation note: Maintain checkbox group behavior + +### Deploy Step Forms +1. **Deployment Status Display** + - Location: Deploy screen + - Purpose: Shows deployment progress + - Integration point: `DeployView.tsx` + - Implementation note: Keep status monitoring logic + +2. **Payment Details Form** + - Location: Final deploy screen + - Purpose: Collect payment information + - Integration point: `DeployView.tsx` (conditional render) + - Implementation note: Preserve all payment processing logic + +## Integration Approach + +1. **Wrapper Component Strategy** + ```tsx + // Example approach for integrating existing forms + const ConfigureView: React.FC = () => { + const { markStepCompleted, goToNextStep, goToPreviousStep } = useNavigationStore() + + // Completion handler that works with the existing form + const handleStepComplete = (formData: any) => { + // Form handled its own submission already + // Just update navigation state + markStepCompleted(OnboardingStep.CONFIGURE) + goToNextStep() + } + + return ( +
+

Configure

+ + {/* Import existing form component without modifying it */} + + + + + {/* Navigation controls added around existing forms */} +
+ + +
+
+ ) + } + ``` + +2. **Event Coordination** + - Listen for existing form submission events + - Update navigation state after form events complete + - Don't modify form validation or submission logic + +3. **Form Data Access** + - Access form data through existing mechanisms (refs, context, etc.) + - Don't introduce new state management for form data + +## Implementation Guidelines + +1. **DO NOT:** + - Change form validation logic + - Modify form submission handlers + - Alter form state management + - Change form UI components + +2. **DO:** + - Wrap existing forms in the appropriate step components + - Add navigation controls outside the forms + - Listen for form completion events + - Update navigation state based on form events + +## Data Flow +1. User interacts with existing form components +2. Forms validate and process data using their existing logic +3. After form processing completes, navigation state is updated +4. Navigation controls the flow between steps + +This approach ensures that the existing form functionality remains unchanged while integrating with the new navigation flow. \ No newline at end of file diff --git a/packages/frontend/src/components/onboarding/docs/State-Management.md b/packages/frontend/src/components/onboarding/docs/State-Management.md new file mode 100644 index 00000000..9a87dce7 --- /dev/null +++ b/packages/frontend/src/components/onboarding/docs/State-Management.md @@ -0,0 +1,124 @@ +# Onboarding Flow State Management + +This document describes the state management approach for the onboarding flow using Zustand. + +## Complete State Management + +The full state management approach includes all aspects of the onboarding flow: + +```typescript +// Define the steps of our onboarding flow +export enum OnboardingStep { + CONNECT = 'connect', + CONFIGURE = 'configure', + DEPLOY = 'deploy' +} + +// Define the repository type +interface Repository { + id: string + name: string + lastUpdated: Date +} + +// Define the template type +interface Template { + id: string + name: string + description: string + type: 'web' | 'api' | 'other' +} + +// Define the deployment configuration type +interface DeploymentConfig { + deploymentUrl?: string + maxDeploys?: number + environmentTypes: { + production: boolean + preview: boolean + development: boolean + } + selectedContainerUrl?: string +} + +// Define our store state +interface OnboardingState { + // Current step in the flow + currentStep: OnboardingStep + + // Step completion status + completedSteps: { + [key in OnboardingStep]: boolean + } + + // GitHub connection status + isConnected: boolean + + // Selected method (import or template) + selectedMethod?: 'import' | 'template' + + // Selected repository (if import method is chosen) + selectedRepository?: Repository + + // Recent repositories (shown in the import list) + recentRepositories: Repository[] + + // Selected template (if template method is chosen) + selectedTemplate?: Template + + // Deployment configuration + deploymentConfig: DeploymentConfig + + // Deployment status + deploymentStatus?: 'configuring' | 'ready' | 'deployed' | 'failed' + + // Actions + setCurrentStep: (step: OnboardingStep) => void + markStepCompleted: (step: OnboardingStep) => void + connect: () => void + disconnect: () => void + selectMethod: (method: 'import' | 'template') => void + selectRepository: (repository: Repository) => void + selectTemplate: (template: Template) => void + updateDeploymentConfig: (config: Partial) => void + setDeploymentStatus: (status: 'configuring' | 'ready' | 'deployed' | 'failed') => void + resetOnboarding: () => void + goToNextStep: () => void + goToPreviousStep: () => void +} +``` + +## Navigation-Only State Management + +For integration with existing form components, a navigation-only state management approach is recommended: + +```typescript +// Define the steps of our onboarding flow +export enum OnboardingStep { + CONNECT = 'connect', + CONFIGURE = 'configure', + DEPLOY = 'deploy' +} + +// Define our navigation state +interface NavigationState { + // Current step in the flow + currentStep: OnboardingStep + + // Step completion status + completedSteps: { + [key in OnboardingStep]: boolean + } + + // Navigation actions + setCurrentStep: (step: OnboardingStep) => void + markStepCompleted: (step: OnboardingStep) => void + markStepIncomplete: (step: OnboardingStep) => void + goToNextStep: () => void + goToPreviousStep: () => void + resetNavigation: () => void + canGoToStep: (step: OnboardingStep) => boolean +} +``` + +The navigation-only approach allows for easier integration with existing form components and state management. \ No newline at end of file diff --git a/packages/frontend/src/components/onboarding/index.ts b/packages/frontend/src/components/onboarding/index.ts new file mode 100644 index 00000000..04ad1d6a --- /dev/null +++ b/packages/frontend/src/components/onboarding/index.ts @@ -0,0 +1,24 @@ +/** + * index.ts + * + * This file exports all components from the onboarding module for easy importing. + */ + +// Export the main layout +export { default as OnboardingLayout } from './OnboardingLayout'; + +// Export the navigation store +export { default as useNavigationStore } from './store/navigationStore'; +export { OnboardingStep } from './store/navigationStore'; + +// Export view components +export { default as ConnectView } from './views/ConnectView'; +export { default as RepositoryListView } from './views/RepositoryListView'; +export { default as TemplateSelectionView } from './views/TemplateSelectionView'; +export { default as ConfigureView } from './views/ConfigureView'; +export { default as DeploymentOptionsView } from './views/DeploymentOptionsView'; +export { default as DeployView } from './views/DeployView'; + +// Export other components +export { default as NavigationSidebar } from './NavigationSidebar'; +export { default as ProgressIndicator } from './ProgressIndicator'; \ No newline at end of file diff --git a/packages/frontend/src/components/onboarding/store/navigationStore.ts b/packages/frontend/src/components/onboarding/store/navigationStore.ts new file mode 100644 index 00000000..ee3f13f4 --- /dev/null +++ b/packages/frontend/src/components/onboarding/store/navigationStore.ts @@ -0,0 +1,175 @@ +/** + * navigationStore.ts + * + * This file implements the Zustand store for navigation state management. + * It focuses on navigation-only state to coordinate between steps. + * + * Implementation: + * 1. Define the OnboardingStep enum for all steps + * 2. Create a NavigationState interface with: + * - currentStep tracking + * - completedSteps status object + * - navigation actions (setCurrentStep, markStepCompleted, etc.) + * 3. Implement the store with Zustand + * 4. Add persistence with localStorage + * 5. Implement navigation logic (can only go to completed steps or next incomplete) + * 6. Add actions for resetting navigation + * + * See the State-Management.md documentation for detailed guidance. + */ + +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +// Define the steps of the onboarding flow +export enum OnboardingStep { + CONNECT = 'connect', + REPOSITORY = 'repository', + TEMPLATE = 'template', + CONFIGURE = 'configure', + DEPLOYMENT_OPTIONS = 'deployment_options', + DEPLOY = 'deploy' +} + +// Define the navigation state interface +interface NavigationState { + // Current step in the flow + currentStep: OnboardingStep; + + // Step completion status + completedSteps: { + [key in OnboardingStep]: boolean; + }; + + // Navigation actions + setCurrentStep: (step: OnboardingStep) => void; + markStepCompleted: (step: OnboardingStep) => void; + markStepIncomplete: (step: OnboardingStep) => void; + goToNextStep: () => void; + goToPreviousStep: () => void; + resetNavigation: () => void; + canGoToStep: (step: OnboardingStep) => boolean; +} + +// Order of steps for navigation +const stepOrder: OnboardingStep[] = [ + OnboardingStep.CONNECT, + OnboardingStep.REPOSITORY, + OnboardingStep.TEMPLATE, + OnboardingStep.CONFIGURE, + OnboardingStep.DEPLOYMENT_OPTIONS, + OnboardingStep.DEPLOY, +]; + +// Create and export the store +const useNavigationStore = create()( + persist( + (set, get) => ({ + // Initial state + currentStep: OnboardingStep.CONNECT, + completedSteps: { + [OnboardingStep.CONNECT]: false, + [OnboardingStep.REPOSITORY]: false, + [OnboardingStep.TEMPLATE]: false, + [OnboardingStep.CONFIGURE]: false, + [OnboardingStep.DEPLOYMENT_OPTIONS]: false, + [OnboardingStep.DEPLOY]: false, + }, + + // Set current step if allowed + setCurrentStep: (step: OnboardingStep) => { + const { canGoToStep } = get(); + if (canGoToStep(step)) { + set({ currentStep: step }); + } + }, + + // Mark a step as completed + markStepCompleted: (step: OnboardingStep) => { + set((state) => ({ + completedSteps: { + ...state.completedSteps, + [step]: true, + }, + })); + }, + + // Mark a step as incomplete + markStepIncomplete: (step: OnboardingStep) => { + set((state) => ({ + completedSteps: { + ...state.completedSteps, + [step]: false, + }, + })); + }, + + // Go to the next step in the flow + goToNextStep: () => { + const { currentStep } = get(); + const currentIndex = stepOrder.indexOf(currentStep); + + if (currentIndex < stepOrder.length - 1) { + const nextStep = stepOrder[currentIndex + 1]; + set({ currentStep: nextStep }); + } + }, + + // Go to the previous step in the flow + goToPreviousStep: () => { + const { currentStep } = get(); + const currentIndex = stepOrder.indexOf(currentStep); + + if (currentIndex > 0) { + const prevStep = stepOrder[currentIndex - 1]; + set({ currentStep: prevStep }); + } + }, + + // Reset navigation state + resetNavigation: () => { + set({ + currentStep: OnboardingStep.CONNECT, + completedSteps: { + [OnboardingStep.CONNECT]: false, + [OnboardingStep.REPOSITORY]: false, + [OnboardingStep.TEMPLATE]: false, + [OnboardingStep.CONFIGURE]: false, + [OnboardingStep.DEPLOYMENT_OPTIONS]: false, + [OnboardingStep.DEPLOY]: false, + }, + }); + }, + + // Check if navigation to a step is allowed + canGoToStep: (step: OnboardingStep) => { + const { completedSteps } = get(); + const stepIndex = stepOrder.indexOf(step); + const currentStepIndex = stepOrder.indexOf(get().currentStep); + + // Can always go to current step or previous completed steps + if (step === get().currentStep || (stepIndex < currentStepIndex && completedSteps[step])) { + return true; + } + + // Can go to the next step if all previous steps are completed + if (stepIndex === currentStepIndex + 1) { + // Check if all previous steps are completed + for (let i = 0; i < stepIndex; i++) { + if (!completedSteps[stepOrder[i]]) { + return false; + } + } + return true; + } + + return false; + }, + }), + { + name: 'onboarding-navigation-storage', + } + ) +); + +export default useNavigationStore; \ No newline at end of file diff --git a/packages/frontend/src/components/onboarding/views/ConfigureView.tsx b/packages/frontend/src/components/onboarding/views/ConfigureView.tsx new file mode 100644 index 00000000..ae42015a --- /dev/null +++ b/packages/frontend/src/components/onboarding/views/ConfigureView.tsx @@ -0,0 +1,163 @@ +/** + * ConfigureView.tsx + * + * This component displays configuration options for deployment. + * It includes settings for deployment URL, environment variables, and other options. + * + * Implementation: + * 1. Display form sections for various configuration options + * 2. Include input fields for deployment URL + * 3. Add environment variables section + * 4. Include deployment options (number of instances, etc.) + * 5. Add navigation controls + */ + +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { ArrowLeft, ArrowRight, Plus, Settings } from 'lucide-react'; +import React, { useState } from 'react'; +import useNavigationStore, { OnboardingStep } from '../store/navigationStore'; + +const ConfigureView: React.FC = () => { + const [deploymentUrl, setDeploymentUrl] = useState(''); + const [maxDeployments, setMaxDeployments] = useState(''); + const [environmentTypes, setEnvironmentTypes] = useState({ + production: true, + preview: true, + development: false, + }); + + const { markStepCompleted, goToNextStep, goToPreviousStep } = useNavigationStore(); + + const handleNext = () => { + // In a real app, would validate inputs + markStepCompleted(OnboardingStep.CONFIGURE); + goToNextStep(); + }; + + const toggleEnvironmentType = (type: 'production' | 'preview' | 'development') => { + setEnvironmentTypes({ + ...environmentTypes, + [type]: !environmentTypes[type], + }); + }; + + return ( +
+ {/* Header */} +
+

Configure

+

+ Set the deployment URL for a single deployment or by creating a separate section for multiple deployments +

+
+ +
+
+ +
+

Configure

+
+ + {/* Configuration Form */} + + + Deployment Settings + + + {/* Deployment URL */} +
+ + setDeploymentUrl(e.target.value)} + /> +
+ + {/* Number of Deployments */} +
+ + +
+ + {/* Environment Variables */} +
+
+ + +
+ {/* Environment variables would be implemented here */} +
+ + {/* Environment Types */} +
+ +
+
+ toggleEnvironmentType('production')} + /> + +
+ +
+ toggleEnvironmentType('preview')} + /> + +
+ +
+ toggleEnvironmentType('development')} + /> + +
+
+
+
+
+ + {/* Navigation Buttons */} +
+ + + +
+
+ ); +}; + +export default ConfigureView; \ No newline at end of file diff --git a/packages/frontend/src/components/onboarding/views/ConnectView.tsx b/packages/frontend/src/components/onboarding/views/ConnectView.tsx new file mode 100644 index 00000000..738bb7a5 --- /dev/null +++ b/packages/frontend/src/components/onboarding/views/ConnectView.tsx @@ -0,0 +1,143 @@ +/** + * ConnectView.tsx + * + * This component displays the initial connection screen for GitHub authentication. + * It shows a button to connect with GitHub and options to select import or template after connection. + * + * Implementation: + * 1. Use existing Card components from shared/ui + * 2. Add a GitHub connect button with icon + * 3. Show two card options after connection: Import Repository or Start with Template + * 4. Add selection behavior for the two options + * 5. Show a "Next" button that's enabled when an option is selected + * 6. Use navigation store to mark step as completed and go to next step + * 7. Match the dark theme UI shown in the screenshots + * + * Dependencies: + * - Card, CardHeader, CardContent, CardFooter from shared components + * - Button from shared components + * - Github icon from Lucide React + * - useNavigationStore + */ + +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; +import { ArrowRight, Code, GitBranch, Github } from 'lucide-react'; +import React, { useState } from 'react'; +import useNavigationStore, { OnboardingStep } from '../store/navigationStore'; + +const ConnectView: React.FC = () => { + const [isConnected, setIsConnected] = useState(false); + const [selectedOption, setSelectedOption] = useState<'import' | 'template' | null>(null); + const { markStepCompleted, goToNextStep, setCurrentStep } = useNavigationStore(); + + // Function to simulate GitHub connection + const handleConnect = () => { + // In a real app, this would trigger GitHub OAuth + setIsConnected(true); + // Mark the connect step as completed + markStepCompleted(OnboardingStep.CONNECT); + }; + + // Function to handle option selection + const handleOptionSelect = (option: 'import' | 'template') => { + setSelectedOption(option); + }; + + // Function to handle navigation to next step + const handleNext = () => { + if (selectedOption === 'import') { + setCurrentStep(OnboardingStep.REPOSITORY); + } else if (selectedOption === 'template') { + setCurrentStep(OnboardingStep.TEMPLATE); + } + }; + + return ( +
+ {/* Header */} +
+

Connect

+

+ Connect and import a GitHub repo or start from a template +

+
+ + {!isConnected ? ( + // Not connected state - Show GitHub connect +
+
+
+ +
+
+ +

Deploy your first app

+

+ Once connected, you can import a repository from your account or start with one of our templates. +

+ + +
+ ) : ( + // Connected state - Show import/template options +
+
+ {/* Import Repository Option */} + handleOptionSelect('import')} + > + +
+ +
+

Import a repository

+

+ Select from your existing GitHub repositories +

+
+
+ + {/* Start with Template Option */} + handleOptionSelect('template')} + > + +
+ +
+

Start with a template

+

+ Choose from our pre-configured templates +

+
+
+
+ + {/* Navigation Buttons */} +
+ +
+
+ )} +
+ ); +}; + +export default ConnectView; \ No newline at end of file diff --git a/packages/frontend/src/components/onboarding/views/DeployView.tsx b/packages/frontend/src/components/onboarding/views/DeployView.tsx new file mode 100644 index 00000000..f985345c --- /dev/null +++ b/packages/frontend/src/components/onboarding/views/DeployView.tsx @@ -0,0 +1,126 @@ +/** + * DeployView.tsx + * + * This component displays the final deployment screen with status information + * and payment options. + * + * Implementation: + * 1. Display deployment status with confirmations + * 2. Show project details + * 3. Include payment or deployment button + * 4. Add navigation controls + * 5. Match the dark theme UI shown in the screenshots + */ + +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; +import { ArrowLeft, ArrowRight, Check, Rocket } from 'lucide-react'; +import React, { useState } from 'react'; +import useNavigationStore, { OnboardingStep } from '../store/navigationStore'; + +const DeployView: React.FC = () => { + const [isDeployed, setIsDeployed] = useState(false); + const { markStepCompleted, goToPreviousStep } = useNavigationStore(); + + const handleDeploy = () => { + // In a real app, this would actually deploy the project + setIsDeployed(true); + markStepCompleted(OnboardingStep.DEPLOY); + }; + + // Mock project data + const project = { + name: 'Progressive Web App (PWA)', + url: 'git.account/repo-name', + status: 'configured', + }; + + return ( +
+ {/* Header */} +
+

Deploy

+

+ Your deployment is configured and ready to go! +

+
+ +
+
+ +
+

Deploy

+
+ + {/* Deployment Status Card */} + + +
+

+ {isDeployed ? 'Deployment Complete!' : 'Your deployment is configured and ready to go!'} +

+

+ {isDeployed + ? 'Your app has been successfully deployed and is now live.' + : 'Review your settings and click "Pay and Deploy" to launch your application.'} +

+
+ + {/* Project Details */} +
+
+
+
+ Progressive Web App (PWA) +
+ $7 per month +
+ +
+ + git.account/repo-name +
+
+ + {/* Deploy Button */} + {!isDeployed ? ( + + ) : ( +
+
+ + Successfully Deployed +
+ +
+ )} +
+
+ + {/* Navigation Buttons */} +
+ + + {isDeployed && ( + + )} +
+
+ ); +}; + +export default DeployView; \ No newline at end of file diff --git a/packages/frontend/src/components/onboarding/views/DeploymentOptionsView.tsx b/packages/frontend/src/components/onboarding/views/DeploymentOptionsView.tsx new file mode 100644 index 00000000..c5929648 --- /dev/null +++ b/packages/frontend/src/components/onboarding/views/DeploymentOptionsView.tsx @@ -0,0 +1,151 @@ +/** + * DeploymentOptionsView.tsx + * + * This component displays advanced deployment options like container selection, + * account selection, and other deployment-specific settings. + * + * Implementation: + * 1. Display forms for deployment options + * 2. Include dropdown for container URL selection + * 3. Include account selection + * 4. Add navigation controls + */ + +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Label } from '@/components/ui/label'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { ArrowLeft, ArrowRight, Server } from 'lucide-react'; +import React, { useState } from 'react'; +import useNavigationStore, { OnboardingStep } from '../store/navigationStore'; + +const DeploymentOptionsView: React.FC = () => { + const [selectedContainer, setSelectedContainer] = useState(''); + const [selectedAccount, setSelectedAccount] = useState(''); + const [environmentTypes, setEnvironmentTypes] = useState({ + production: true, + preview: true, + development: false, + }); + + const { markStepCompleted, goToNextStep, goToPreviousStep } = useNavigationStore(); + + const handleNext = () => { + // In a real app, would validate inputs + markStepCompleted(OnboardingStep.DEPLOYMENT_OPTIONS); + goToNextStep(); + }; + + const toggleEnvironmentType = (type: 'production' | 'preview' | 'development') => { + setEnvironmentTypes({ + ...environmentTypes, + [type]: !environmentTypes[type], + }); + }; + + return ( +
+ {/* Header */} +
+

Configure

+

+ Set the deployment URL for a single deployment or by creating a separate section for multiple deployments +

+
+ +
+
+ +
+

Deployment Options

+
+ + {/* Deployment Options Form */} + + + Container Settings + + + {/* Container URL */} +
+ + +
+ + {/* Environment Types */} +
+ +
+
+ toggleEnvironmentType('production')} + /> + +
+ +
+ toggleEnvironmentType('preview')} + /> + +
+ +
+ toggleEnvironmentType('development')} + /> + +
+
+
+ + {/* Account Selection */} +
+ + +
+
+
+ + {/* Navigation Buttons */} +
+ + + +
+
+ ); +}; + +export default DeploymentOptionsView; \ No newline at end of file diff --git a/packages/frontend/src/components/onboarding/views/RepositoryListView.tsx b/packages/frontend/src/components/onboarding/views/RepositoryListView.tsx new file mode 100644 index 00000000..8aaa45f8 --- /dev/null +++ b/packages/frontend/src/components/onboarding/views/RepositoryListView.tsx @@ -0,0 +1,106 @@ +/** + * RepositoryListView.tsx + * + * This component displays a list of GitHub repositories for the user to select from. + * It shows repository names with timestamps and allows selection. + * + * Implementation: + * 1. Use a scrollable list component + * 2. Show repository names with timestamps + * 3. Allow selection with radio buttons + * 4. Add navigation controls (back/next) + * 5. Add proper spacing and responsive behavior + */ + +import { Button } from '@/components/ui/button'; +import { Label } from '@/components/ui/label'; +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { ArrowLeft, ArrowRight, GitBranch } from 'lucide-react'; +import React, { useState } from 'react'; +import useNavigationStore, { OnboardingStep } from '../store/navigationStore'; + +// Mock data for repositories +const mockRepositories = [ + { id: '1', name: 'username/repo-name', updatedAt: '4 minutes ago' }, + { id: '2', name: 'username/another-repo', updatedAt: '6 minutes ago' }, + { id: '3', name: 'username/test-project', updatedAt: '2 hours ago' }, + { id: '4', name: 'username/awesome-app', updatedAt: '1 day ago' }, + { id: '5', name: 'username/frontend-demo', updatedAt: '3 days ago' }, +]; + +const RepositoryListView: React.FC = () => { + const [selectedRepo, setSelectedRepo] = useState(null); + const { markStepCompleted, goToNextStep, goToPreviousStep } = useNavigationStore(); + + const handleRepoSelect = (repoId: string) => { + setSelectedRepo(repoId); + }; + + const handleNext = () => { + if (selectedRepo) { + markStepCompleted(OnboardingStep.REPOSITORY); + goToNextStep(); + } + }; + + return ( +
+ {/* Header */} +
+

Connect

+

+ Connect and import a GitHub repo or start from a template +

+
+ +
+
+ +
+

Import a repository

+
+ + {/* Repository List */} +
+ + + {mockRepositories.map((repo) => ( +
+ + +
+ ))} +
+
+
+ + {/* Navigation Buttons */} +
+ + + +
+
+ ); +}; + +export default RepositoryListView; \ No newline at end of file diff --git a/packages/frontend/src/components/onboarding/views/TemplateSelectionView.tsx b/packages/frontend/src/components/onboarding/views/TemplateSelectionView.tsx new file mode 100644 index 00000000..1941dd0b --- /dev/null +++ b/packages/frontend/src/components/onboarding/views/TemplateSelectionView.tsx @@ -0,0 +1,146 @@ +/** + * TemplateSelectionView.tsx + * + * This component displays a grid of templates for the user to select from. + * It shows template cards with icons, names, and descriptions. + * + * Implementation: + * 1. Display a grid of template cards + * 2. Show template details including icon, name, and description + * 3. Allow selection of a template + * 4. Add navigation controls + * 5. Add proper spacing and responsive behavior + */ + +import { Button } from '@/components/ui/button'; +import { Card, CardContent } from '@/components/ui/card'; +import { ArrowLeft, ArrowRight, Code } from 'lucide-react'; +import React, { useState } from 'react'; +import useNavigationStore, { OnboardingStep } from '../store/navigationStore'; + +// Mock data for templates +const mockTemplates = [ + { + id: '1', + name: 'Progressive Web App (PWA)', + description: 'A responsive web app with offline capabilities', + icon: Code, + category: 'web' + }, + { + id: '2', + name: 'Design Upload Pack', + description: 'Simple file upload and processing pipeline', + icon: Code, + category: 'utility' + }, + { + id: '3', + name: 'React + Redux + TailwindCSS', + description: 'Modern frontend stack with state management', + icon: Code, + category: 'web' + }, + { + id: '4', + name: 'Node.js API Starter', + description: 'Backend API with Express and MongoDB', + icon: Code, + category: 'api' + }, + { + id: '5', + name: 'E-commerce Platform', + description: 'Full-stack shop with payment processing', + icon: Code, + category: 'fullstack' + }, + { + id: '6', + name: 'Static Blog Starter', + description: 'JAMstack blog with markdown support', + icon: Code, + category: 'content' + }, +]; + +const TemplateSelectionView: React.FC = () => { + const [selectedTemplate, setSelectedTemplate] = useState(null); + const { markStepCompleted, goToNextStep, goToPreviousStep } = useNavigationStore(); + + const handleTemplateSelect = (templateId: string) => { + setSelectedTemplate(templateId); + }; + + const handleNext = () => { + if (selectedTemplate) { + markStepCompleted(OnboardingStep.TEMPLATE); + goToNextStep(); + } + }; + + return ( +
+ {/* Header */} +
+

Connect

+

+ Connect and import a GitHub repo or start from a template +

+
+ +
+
+ +
+

Start with a template

+
+ + {/* Template Grid */} +
+ {mockTemplates.map((template) => { + const Icon = template.icon; + const isSelected = selectedTemplate === template.id; + + return ( + handleTemplateSelect(template.id)} + > + +
+ +
+
+

{template.name}

+

{template.description}

+
+
+
+ ); + })} +
+ + {/* Navigation Buttons */} +
+ + + +
+
+ ); +}; + +export default TemplateSelectionView; \ No newline at end of file diff --git a/packages/frontend/src/pages/AuthPage.tsx b/packages/frontend/src/pages/AuthPage.tsx index 57577656..4cb72ca6 100644 --- a/packages/frontend/src/pages/AuthPage.tsx +++ b/packages/frontend/src/pages/AuthPage.tsx @@ -1,4 +1,6 @@ +import OnboardingDialog from '@/components/onboarding/OnboardingDialog'; import AutoSignInIFrameModal from '@/components/shared/auth/AutoSignInIFrameModal'; +import { Button } from '@/components/ui/button'; /** * AuthPage component that renders the authentication page with an auto sign-in iframe modal. @@ -17,8 +19,18 @@ const AuthPage = () => { />
-
+
+
+

Want to see our new onboarding flow?

+ + Try Onboarding Demo + + } + /> +
); diff --git a/packages/frontend/src/pages/OnboardingDemoPage.tsx b/packages/frontend/src/pages/OnboardingDemoPage.tsx new file mode 100644 index 00000000..f33c8900 --- /dev/null +++ b/packages/frontend/src/pages/OnboardingDemoPage.tsx @@ -0,0 +1,57 @@ +import OnboardingDialog from '@/components/onboarding/OnboardingDialog'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import React, { useState } from 'react'; + +/** + * OnboardingDemoPage component + * + * This page serves as a landing page with a prominent button to open the onboarding dialog. + * It provides information about the onboarding feature and launches it in a modal dialog. + */ +const OnboardingDemoPage: React.FC = () => { + const [dialogOpen, setDialogOpen] = useState(false); + + return ( +
+ + + Onboarding Demo + + Experience the new user onboarding flow for Snowball deployment + + + +
+

+ The onboarding flow guides users through the following steps: +

+
    +
  • Connecting with GitHub
  • +
  • Selecting a repository or template
  • +
  • Configuring deployment settings
  • +
  • Setting deployment options
  • +
  • Deploying your application
  • +
+

+ All data in this demo is simulated and no actual deployments will be made. +

+
+ +
+ + Open Onboarding Dialog + + } + defaultOpen={dialogOpen} + /> +
+
+
+
+ ); +}; + +export default OnboardingDemoPage; \ No newline at end of file diff --git a/packages/frontend/src/pages/OnboardingPage.tsx b/packages/frontend/src/pages/OnboardingPage.tsx new file mode 100644 index 00000000..de167598 --- /dev/null +++ b/packages/frontend/src/pages/OnboardingPage.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import OnboardingLayout from '../components/onboarding/OnboardingLayout'; + +/** + * OnboardingPage component + * + * This page serves as a demo wrapper for the onboarding feature. + * It simply renders the OnboardingLayout component which handles + * the entire onboarding flow. + */ +const OnboardingPage: React.FC = () => { + return ; +}; + +export default OnboardingPage; \ No newline at end of file From c06d76ed74db0485b2abf23c7bd4b35012876126 Mon Sep 17 00:00:00 2001 From: icld Date: Wed, 26 Feb 2025 11:43:21 -0800 Subject: [PATCH 2/3] Onboarding documented, begin consume existing components --- .gitignore | 4 +- README.md | 18 +- packages/frontend/.gitignore | 4 +- packages/frontend/package.json | 4 +- packages/frontend/src/App.tsx | 4 +- packages/frontend/src/app/layout.tsx | 17 + packages/frontend/src/app/page.tsx | 74 +++ packages/frontend/src/components/Dropdown.tsx | 24 + .../src/components/FormatMilliSecond.tsx | 17 + .../src/components/HorizontalLine.tsx | 11 + packages/frontend/src/components/Logo.tsx | 17 + .../frontend/src/components/SearchBar.tsx | 12 + packages/frontend/src/components/Stepper.tsx | 25 + .../frontend/src/components/StopWatch.tsx | 20 +- .../src/components/VerticalStepper.tsx | 25 + .../src/components/examples/ExamplePage.tsx | 19 + .../layout/navigation/NavigationWrapper.tsx | 3 + .../layout/navigation/TopNavigation.tsx | 4 + .../onboarding-flow/OPTIMIZATION.md | 117 ++++ .../components/onboarding-flow/Onboarding.tsx | 81 +++ .../src/components/onboarding-flow/README.md | 109 ++++ .../onboarding-flow/common/background-svg.tsx | 13 + .../onboarding-flow/common/index.ts | 13 + .../common/laconic-icon-lettering.tsx | 42 ++ .../common/onboarding-container.tsx | 34 ++ .../onboarding-flow/common/step-header.tsx | 46 ++ .../common/step-navigation.tsx | 80 +++ .../configure-step/configure-step.tsx | 64 +++ .../onboarding-flow/configure-step/index.ts | 9 + .../connect-step/connect-button.tsx | 52 ++ .../connect-step/connect-deploy-first-app.tsx | 39 ++ .../connect-step/connect-initial.tsx | 33 ++ .../connect-step/connect-step.tsx | 61 ++ .../onboarding-flow/connect-step/index.ts | 14 + .../connect-step/repository-list.tsx | 44 ++ .../connect-step/template-list.tsx | 51 ++ .../deploy-step/deploy-step.tsx | 49 ++ .../onboarding-flow/deploy-step/index.ts | 9 + .../src/components/onboarding-flow/index.ts | 29 + .../onboarding-flow/sidebar/index.ts | 9 + .../onboarding-flow/sidebar/sidebar-nav.tsx | 138 +++++ .../src/components/onboarding-flow/store.ts | 61 ++ .../src/components/onboarding-flow/types.ts | 83 +++ .../onboarding-flow/useOnboarding.ts | 41 ++ .../onboarding/NavigationSidebar.tsx | 14 +- .../onboarding/OnboardingDialog.tsx | 9 +- .../projects/ProjectCard/ProjectCard.theme.ts | 18 +- .../projects/ProjectCard/ProjectCard.tsx | 228 +++----- .../ProjectCard/ProjectCardActions.tsx | 69 +++ .../ProjectCard/ProjectDeploymentInfo.tsx | 61 ++ .../projects/ProjectCard/ProjectStatusDot.tsx | 52 ++ .../frontend/src/context/OctokitContext.tsx | 10 +- packages/frontend/src/index.css | 11 + .../frontend/src/layouts/DashboardLayout.tsx | 1 + standards/COMPONENT_DOCUMENTATION.md | 525 ++++++++++++++++++ standards/FEATURE_BUILDING.md | 232 ++++++++ standards/FEATURE_BUILDING_TEMPLATE.md | 239 ++++++++ standards/README.md | 41 ++ yarn.lock | 10 + 59 files changed, 2979 insertions(+), 164 deletions(-) create mode 100644 packages/frontend/src/app/layout.tsx create mode 100644 packages/frontend/src/app/page.tsx create mode 100644 packages/frontend/src/components/onboarding-flow/OPTIMIZATION.md create mode 100644 packages/frontend/src/components/onboarding-flow/Onboarding.tsx create mode 100644 packages/frontend/src/components/onboarding-flow/README.md create mode 100644 packages/frontend/src/components/onboarding-flow/common/background-svg.tsx create mode 100644 packages/frontend/src/components/onboarding-flow/common/index.ts create mode 100644 packages/frontend/src/components/onboarding-flow/common/laconic-icon-lettering.tsx create mode 100644 packages/frontend/src/components/onboarding-flow/common/onboarding-container.tsx create mode 100644 packages/frontend/src/components/onboarding-flow/common/step-header.tsx create mode 100644 packages/frontend/src/components/onboarding-flow/common/step-navigation.tsx create mode 100644 packages/frontend/src/components/onboarding-flow/configure-step/configure-step.tsx create mode 100644 packages/frontend/src/components/onboarding-flow/configure-step/index.ts create mode 100644 packages/frontend/src/components/onboarding-flow/connect-step/connect-button.tsx create mode 100644 packages/frontend/src/components/onboarding-flow/connect-step/connect-deploy-first-app.tsx create mode 100644 packages/frontend/src/components/onboarding-flow/connect-step/connect-initial.tsx create mode 100644 packages/frontend/src/components/onboarding-flow/connect-step/connect-step.tsx create mode 100644 packages/frontend/src/components/onboarding-flow/connect-step/index.ts create mode 100644 packages/frontend/src/components/onboarding-flow/connect-step/repository-list.tsx create mode 100644 packages/frontend/src/components/onboarding-flow/connect-step/template-list.tsx create mode 100644 packages/frontend/src/components/onboarding-flow/deploy-step/deploy-step.tsx create mode 100644 packages/frontend/src/components/onboarding-flow/deploy-step/index.ts create mode 100644 packages/frontend/src/components/onboarding-flow/index.ts create mode 100644 packages/frontend/src/components/onboarding-flow/sidebar/index.ts create mode 100644 packages/frontend/src/components/onboarding-flow/sidebar/sidebar-nav.tsx create mode 100644 packages/frontend/src/components/onboarding-flow/store.ts create mode 100644 packages/frontend/src/components/onboarding-flow/types.ts create mode 100644 packages/frontend/src/components/onboarding-flow/useOnboarding.ts create mode 100644 packages/frontend/src/components/projects/ProjectCard/ProjectCardActions.tsx create mode 100644 packages/frontend/src/components/projects/ProjectCard/ProjectDeploymentInfo.tsx create mode 100644 packages/frontend/src/components/projects/ProjectCard/ProjectStatusDot.tsx create mode 100644 standards/COMPONENT_DOCUMENTATION.md create mode 100644 standards/FEATURE_BUILDING.md create mode 100644 standards/FEATURE_BUILDING_TEMPLATE.md create mode 100644 standards/README.md diff --git a/.gitignore b/.gitignore index 76344a89..144bb309 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,6 @@ packages/frontend/dist/ # ignore all .DS_Store files **/.DS_Store -.vscode \ No newline at end of file +.vscode + +.cursor/ diff --git a/README.md b/README.md index 7a28e9c7..61ac548c 100644 --- a/README.md +++ b/README.md @@ -20,4 +20,20 @@ yarn build --ignore frontend ### Environment variables, running the development server, and deployment -Follow the instructions in the README.md files of the [backend](packages/backend/README.md) and [frontend](packages/frontend/README.md) packages. +Follow the instructions in the README.md files of the [backend](packages/backend/README.md) and +[frontend](packages/frontend/README.md) packages. + +## Development Guidelines + +### Project Standards + +We maintain a set of project-wide standards and conventions in the [standards](./standards) +directory. These standards help ensure consistency across the codebase and make it easier for +developers to collaborate. + +Current standards: + +- [Component Documentation Standards](./standards/COMPONENT_DOCUMENTATION.md) - Guidelines for + documenting components, hooks, and utilities +- [Feature Building Process](./standards/FEATURE_BUILDING.md) - Standardized approach to building + new features from design to implementation diff --git a/packages/frontend/.gitignore b/packages/frontend/.gitignore index 05a41290..0c2b61a1 100644 --- a/packages/frontend/.gitignore +++ b/packages/frontend/.gitignore @@ -22,4 +22,6 @@ npm-debug.log* yarn-debug.log* yarn-error.log* -*storybook.log \ No newline at end of file +*storybook.log + +.cursor/ \ No newline at end of file diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 9dfb2aed..5e98039e 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -32,6 +32,7 @@ "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-hover-card": "^1.1.6", + "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-menubar": "^1.1.6", "@radix-ui/react-navigation-menu": "^1.2.5", @@ -100,7 +101,8 @@ "vaul": "^1.1.2", "viem": "^2.7.11", "web-vitals": "^2.1.4", - "zod": "^3.24.2" + "zod": "^3.24.2", + "zustand": "^5.0.3" }, "devDependencies": { "@chromatic-com/storybook": "^1.3.3", diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index 33ec9cb0..68118da8 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -18,10 +18,10 @@ const router = createBrowserRouter([ children: [ // { // element: , - // children: [ + // children: [` // { // path: '', - // element: , + // element: ,` // }, // { // path: 'projects', diff --git a/packages/frontend/src/app/layout.tsx b/packages/frontend/src/app/layout.tsx new file mode 100644 index 00000000..dbeabcf1 --- /dev/null +++ b/packages/frontend/src/app/layout.tsx @@ -0,0 +1,17 @@ +import type React from "react" +import "@/styles/globals.css" + +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + +
{children}
+ + + ) +} + diff --git a/packages/frontend/src/app/page.tsx b/packages/frontend/src/app/page.tsx new file mode 100644 index 00000000..9496910c --- /dev/null +++ b/packages/frontend/src/app/page.tsx @@ -0,0 +1,74 @@ +import { + OnboardingContainer, + SidebarNav, + StepHeader, + StepNavigation, +} from '@/components/onboarding-flow'; +import { ConfigureStep } from '@/components/onboarding-flow/configure-step/configure-step'; +import { ConnectStep } from '@/components/onboarding-flow/connect-step/connect-step'; +import { DeployStep } from '@/components/onboarding-flow/deploy-step/deploy-step'; +import { useOnboarding } from '@/components/onboarding-flow/store'; +import { FileCog, GitPullRequest, SquareArrowOutDownRight } from 'lucide-react'; + +/** Icons for each step in the onboarding flow */ +const stepIcons = { + connect: , + configure: , + deploy: , +}; + +/** Titles for each step in the onboarding flow */ +const stepTitles = { + connect: 'Connect', + configure: 'Configure', + deploy: 'Deploy', +}; + +/** Descriptions for each step in the onboarding flow */ +const stepDescriptions = { + connect: 'Connect and import a GitHub repository to start deploying.', + configure: 'Set up your deployment configuration and environment variables.', + deploy: 'Review your settings and deploy your project.', +}; + +/** + * Main onboarding page component + * Orchestrates the entire onboarding flow and manages step transitions + * + * Component Hierarchy: + * - OnboardingContainer + * - SidebarNav (step progress) + * - Main content + * - StepHeader (current step info) + * - Step content (ConnectStep | ConfigureStep | DeployStep) + * - StepNavigation (previous/next controls) + * + * @returns {JSX.Element} Complete onboarding interface + */ +export default function Page() { + const { currentStep, nextStep, previousStep } = useOnboarding(); + + return ( + + +
+ +
+ {currentStep === 'connect' && } + {currentStep === 'configure' && } + {currentStep === 'deploy' && } +
+ +
+
+ ); +} diff --git a/packages/frontend/src/components/Dropdown.tsx b/packages/frontend/src/components/Dropdown.tsx index 5660bea3..964bb384 100644 --- a/packages/frontend/src/components/Dropdown.tsx +++ b/packages/frontend/src/components/Dropdown.tsx @@ -9,6 +9,14 @@ export interface Option { label: string; } +/** + * Props for the Dropdown component. + * @interface DropdownProps + * @property {Option[]} options - The list of options to display in the dropdown. + * @property {(arg: ReactDropdownOption) => void} onChange - Callback fired when an option is selected. + * @property {string} [placeholder] - Placeholder text for the dropdown. + * @property {Option} [value] - The currently selected option. + */ interface DropdownProps { options: Option[]; onChange: (arg: ReactDropdownOption) => void; @@ -16,6 +24,22 @@ interface DropdownProps { value?: Option; } +/** + * A dropdown component that wraps the ReactDropdown library. + * + * @component + * @param {DropdownProps} props - The props for the Dropdown component. + * @returns {React.ReactElement} A dropdown element. + * + * @example + * ```tsx + * console.log(option)} + * placeholder="Select an option" + * /> + * ``` + */ const Dropdown = ({ placeholder, options, onChange, value }: DropdownProps) => { return ( { time: number; } +/** + * A component that formats a given time in milliseconds into a human-readable format. + * + * @component + * @param {FormatMilliSecondProps} props - The props for the FormatMillisecond component. + * @returns {React.ReactElement} A formatted time element. + * + * @example + * ```tsx + * + * ``` + */ const FormatMillisecond = ({ time, ...props }: FormatMilliSecondProps) => { const formatTime = Duration.fromMillis(time) .shiftTo('days', 'hours', 'minutes', 'seconds') diff --git a/packages/frontend/src/components/HorizontalLine.tsx b/packages/frontend/src/components/HorizontalLine.tsx index 62c6242f..0f917193 100644 --- a/packages/frontend/src/components/HorizontalLine.tsx +++ b/packages/frontend/src/components/HorizontalLine.tsx @@ -1,3 +1,14 @@ +/** + * A simple horizontal line component. + * + * @component + * @returns {React.ReactElement} A horizontal line element. + * + * @example + * ```tsx + * + * ``` + */ const HorizontalLine = () => { return
; }; diff --git a/packages/frontend/src/components/Logo.tsx b/packages/frontend/src/components/Logo.tsx index f4957f14..9c7f7808 100644 --- a/packages/frontend/src/components/Logo.tsx +++ b/packages/frontend/src/components/Logo.tsx @@ -1,9 +1,26 @@ import { Link } from 'react-router-dom'; +/** + * Props for the Logo component. + * @interface LogoProps + * @property {string} [orgSlug] - The organization slug used for the link. + */ interface LogoProps { orgSlug?: string; } +/** + * A component that renders the Snowball logo with a link to the organization's page. + * + * @component + * @param {LogoProps} props - The props for the Logo component. + * @returns {React.ReactElement} A logo element. + * + * @example + * ```tsx + * + * ``` + */ export const Logo = ({ orgSlug }: LogoProps) => { return ( diff --git a/packages/frontend/src/components/SearchBar.tsx b/packages/frontend/src/components/SearchBar.tsx index 2ba5c2e4..88f21efa 100644 --- a/packages/frontend/src/components/SearchBar.tsx +++ b/packages/frontend/src/components/SearchBar.tsx @@ -3,6 +3,18 @@ import React, { forwardRef, RefAttributes } from 'react'; import { IconInput, InputProps } from '@/components/ui'; import { Search } from 'lucide-react'; +/** + * A search bar component with an icon input. + * + * @component + * @param {InputProps & RefAttributes} props - The props for the SearchBar component. + * @returns {React.ReactElement} A search bar element. + * + * @example + * ```tsx + * console.log(e.target.value)} /> + * ``` + */ const SearchBar: React.ForwardRefRenderFunction< HTMLInputElement, InputProps & RefAttributes diff --git a/packages/frontend/src/components/Stepper.tsx b/packages/frontend/src/components/Stepper.tsx index 31e7e717..f462bdf5 100644 --- a/packages/frontend/src/components/Stepper.tsx +++ b/packages/frontend/src/components/Stepper.tsx @@ -4,17 +4,42 @@ const COLOR_COMPLETED = '#059669'; const COLOR_ACTIVE = '#CFE6FC'; const COLOR_NOT_STARTED = '#F1F5F9'; +/** + * Represents a step in the stepper. + * @interface StepperValue + * @property {number} step - The step number. + * @property {string} route - The route associated with the step. + * @property {string} label - The label for the step. + */ interface StepperValue { step: number; route: string; label: string; } +/** + * Props for the Stepper component. + * @interface StepperProps + * @property {number} activeStep - The currently active step. + * @property {StepperValue[]} stepperValues - The values for each step. + */ interface StepperProps { activeStep: number; stepperValues: StepperValue[]; } +/** + * A stepper component that displays a series of steps with different states. + * + * @component + * @param {StepperProps} props - The props for the Stepper component. + * @returns {React.ReactElement} A stepper element. + * + * @example + * ```tsx + * + * ``` + */ const Stepper = ({ activeStep, stepperValues }: StepperProps) => { return ( { return currentTime; }; +/** + * Props for the Stopwatch component. + * @interface StopwatchProps + * @property {Date} offsetTimestamp - The initial timestamp for the stopwatch. + * @property {boolean} isPaused - Whether the stopwatch is paused. + */ interface StopwatchProps extends Omit { offsetTimestamp: Date; isPaused: boolean; } +/** + * A stopwatch component that tracks elapsed time. + * + * @component + * @param {StopwatchProps} props - The props for the Stopwatch component. + * @returns {React.ReactElement} A stopwatch element. + * + * @example + * ```tsx + * + * ``` + */ const Stopwatch = ({ offsetTimestamp, isPaused, ...props }: StopwatchProps) => { const { totalSeconds, pause, start } = useStopwatch({ autoStart: true, @@ -29,4 +47,4 @@ const Stopwatch = ({ offsetTimestamp, isPaused, ...props }: StopwatchProps) => { return ; }; -export { Stopwatch, setStopWatchOffset }; +export { setStopWatchOffset, Stopwatch }; diff --git a/packages/frontend/src/components/VerticalStepper.tsx b/packages/frontend/src/components/VerticalStepper.tsx index 43ff80a4..73aff806 100644 --- a/packages/frontend/src/components/VerticalStepper.tsx +++ b/packages/frontend/src/components/VerticalStepper.tsx @@ -3,6 +3,14 @@ import * as CSS from 'csstype'; // // Nav // +/** + * Describes a step in the stepper navigation. + * @interface IStepDescription + * @property {() => JSX.Element} stepContent - The content of the step. + * @property {string} [stepStateColor] - The color representing the step's state. + * @property {number} [stepStatusCircleSize] - The size of the status circle. + * @property {() => void} [onClickHandler] - Handler for click events on the step. + */ export interface IStepDescription { stepContent: () => JSX.Element; stepStateColor?: string; @@ -10,10 +18,27 @@ export interface IStepDescription { onClickHandler?: () => void | undefined; } +/** + * Props for the StepperNav component. + * @interface IStepperNavProps + * @property {IStepDescription[]} steps - The steps to display in the navigation. + */ export interface IStepperNavProps { steps: IStepDescription[]; } +/** + * A navigation component for displaying steps in a vertical layout. + * + * @component + * @param {IStepperNavProps} props - The props for the StepperNav component. + * @returns {React.ReactElement} A stepper navigation element. + * + * @example + * ```tsx + *
Step 1
}]} /> + * ``` + */ export const StepperNav = (props: IStepperNavProps): JSX.Element => { return (
@@ -143,11 +237,37 @@ export function TopNavigation() {
+ {/* Add New Button (Mobile) */} + + + + + + {isGitHubConnected && ( + <> + + Create New + + + + )} + + Run Onboarding + + +
+ + {/* Onboarding Dialog */} + {showOnboarding && ( + + )} ); } diff --git a/packages/frontend/src/components/onboarding-flow/Onboarding.tsx b/packages/frontend/src/components/onboarding-flow/Onboarding.tsx index dec80943..abef6bcf 100644 --- a/packages/frontend/src/components/onboarding-flow/Onboarding.tsx +++ b/packages/frontend/src/components/onboarding-flow/Onboarding.tsx @@ -6,15 +6,14 @@ */ import { - OnboardingContainer, - StepHeader, - StepNavigation, + OnboardingContainer, StepNavigation } from '@/components/onboarding-flow/common'; import { ConfigureStep } from '@/components/onboarding-flow/configure-step'; import { ConnectStep } from '@/components/onboarding-flow/connect-step'; import { DeployStep } from '@/components/onboarding-flow/deploy-step'; import { SidebarNav } from '@/components/onboarding-flow/sidebar'; import { useOnboarding } from '@/components/onboarding-flow/store'; +import { ScrollArea } from '@/components/ui/scroll-area'; import { FileCog, GitPullRequest, SquareArrowOutDownRight } from 'lucide-react'; /** Icons for each step in the onboarding flow */ @@ -58,17 +57,19 @@ export default function Onboarding() { return ( -
- + {/* -
- {currentStep === 'connect' && } - {currentStep === 'configure' && } - {currentStep === 'deploy' && } -
+ /> */} +
+ + {currentStep === 'connect' && } + {currentStep === 'configure' && } + {currentStep === 'deploy' && } + +
void; +} + +/** + * OnboardingDialog component + * + * A dialog modal that contains the onboarding flow. + * Can be triggered by a custom element or automatically opened. + * Sets the initial step based on GitHub connection status. + * Provides warnings when exiting mid-step and options to continue progress. + */ +const OnboardingDialog: React.FC = ({ + trigger, + defaultOpen = false, + onClose +}) => { + const onboardingStore = useOnboarding(); + const { setCurrentStep, currentStep, formData } = onboardingStore; + const { octokit } = useOctokit(); + const [showExitWarning, setShowExitWarning] = useState(false); + const [showContinueAlert, setShowContinueAlert] = useState(false); + const [isOpen, setIsOpen] = useState(defaultOpen); + const [forceConnectStep, setForceConnectStep] = useState(false); + + // Check for force connect flag when component mounts + useEffect(() => { + const shouldForceConnect = localStorage.getItem(ONBOARDING_FORCE_CONNECT_KEY) === 'true'; + if (shouldForceConnect) { + setForceConnectStep(true); + // Clear the flag so it's only used once + localStorage.removeItem(ONBOARDING_FORCE_CONNECT_KEY); + } + }, []); + + // Local implementation of reset function that handles all necessary state + const resetOnboardingState = () => { + // Reset step to connect + setCurrentStep('connect'); + + // Flag to force starting from the connect step + setForceConnectStep(true); + + // Also reset form data to ensure substeps are cleared + const store = onboardingStore as any; + if (typeof store.updateFormData === 'function') { + store.updateFormData({ + projectName: '', + repoName: '', + repoDescription: '', + framework: '', + access: 'public', + organizationSlug: '', + }); + } + }; + + // Check if there's existing progress + useEffect(() => { + if (isOpen) { + const savedProgress = localStorage.getItem(ONBOARDING_PROGRESS_KEY); + const savedState = localStorage.getItem(ONBOARDING_STATE_KEY); + + if (savedProgress === 'true' && savedState && !forceConnectStep) { + // Show continue or start fresh dialog + setShowContinueAlert(true); + } else { + // Set initial step based on GitHub connection status + initializeOnboarding(); + } + } + }, [isOpen, forceConnectStep]); + + // Set the initial step based on GitHub connection status + const initializeOnboarding = () => { + // Reset previous state + resetOnboardingState(); + + // If GitHub is connected AND we're not forcing the connect step, + // start at the configure step. Otherwise, start at the connect step + if (octokit && !forceConnectStep) { + setCurrentStep('configure'); + } else { + setCurrentStep('connect'); + } + + // Mark that we have onboarding in progress + localStorage.setItem(ONBOARDING_PROGRESS_KEY, 'true'); + + // Save the initial state + saveCurrentState(); + }; + + // Start fresh by initializing onboarding and forcing the connect step + const startFresh = () => { + // Set flag to force starting from the connect step + setForceConnectStep(true); + initializeOnboarding(); + setShowContinueAlert(false); + }; + + // Continue from saved state and don't force the connect step + const continueOnboarding = () => { + // Reset the force flag since we're continuing + setForceConnectStep(false); + loadSavedState(); + }; + + // Save current onboarding state + const saveCurrentState = () => { + try { + const state = { + currentStep, + formData, + forceConnectStep // Save this flag as part of the state + }; + localStorage.setItem(ONBOARDING_STATE_KEY, JSON.stringify(state)); + } catch (error) { + console.error('Error saving onboarding state:', error); + } + }; + + // Load saved onboarding state + const loadSavedState = () => { + try { + const savedState = localStorage.getItem(ONBOARDING_STATE_KEY); + if (savedState) { + const state = JSON.parse(savedState); + + // Restore the force flag if it exists + if (state.forceConnectStep !== undefined) { + setForceConnectStep(state.forceConnectStep); + } + + setCurrentStep(state.currentStep as Step); + + // Also restore form data to preserve org/repo selection + const store = onboardingStore as any; + if (state.formData && typeof store.updateFormData === 'function') { + store.updateFormData(state.formData as Partial); + } + } + } catch (error) { + console.error('Error loading onboarding state:', error); + initializeOnboarding(); + } + setShowContinueAlert(false); + }; + + // Save state on step changes + useEffect(() => { + if (isOpen) { + saveCurrentState(); + } + }, [currentStep, formData, forceConnectStep]); + + // Mark onboarding as completed when user reaches the deploy step + useEffect(() => { + if (currentStep === 'deploy') { + localStorage.setItem(ONBOARDING_COMPLETED_KEY, 'true'); + } + }, [currentStep]); + + // Handle dialog close attempt + const handleOpenChange = (open: boolean) => { + if (!open && isOpen) { + // If closing and not on the last step, show warning + if (currentStep !== 'deploy') { + setShowExitWarning(true); + return; // Prevent closing until user confirms + } + + // If on the last step or user confirmed, close normally + completeClose(); + } else { + setIsOpen(open); + } + }; + + // Complete the closing process + const completeClose = () => { + // Mark as completed when dialog is closed + localStorage.setItem(ONBOARDING_COMPLETED_KEY, 'true'); + + // Clear progress flag + localStorage.removeItem(ONBOARDING_PROGRESS_KEY); + localStorage.removeItem(ONBOARDING_STATE_KEY); + + // Reset onboarding state for next time + resetOnboardingState(); + + // Close the dialog + setIsOpen(false); + + if (onClose) { + onClose(); + } + }; + + // Cancel closing + const cancelClose = () => { + setShowExitWarning(false); + }; + + return ( + <> + + {trigger && {trigger}} + +
+ +
+
+
+ + {/* Exit Warning Dialog */} + + + + Exit Onboarding? + + You haven't completed the onboarding process. If you exit now, your progress will be lost, including any organization or repository selections. + + + + Cancel + Exit Anyway + + + + + {/* Continue Progress Dialog */} + + + + Continue Onboarding? + + You're in the middle of setting up your project, including organization and repository selection. Would you like to continue where you left off or start fresh? + + + + Start Fresh + Continue + + + + + ); +}; + +/** + * Helper function to check if the user has completed onboarding + * @returns {boolean} Whether onboarding has been completed + */ +export const hasCompletedOnboarding = (): boolean => { + return localStorage.getItem(ONBOARDING_COMPLETED_KEY) === 'true'; +}; + +export default OnboardingDialog; \ No newline at end of file diff --git a/packages/frontend/src/components/onboarding-flow/common/onboarding-container.tsx b/packages/frontend/src/components/onboarding-flow/common/onboarding-container.tsx index b346f03e..68b64165 100644 --- a/packages/frontend/src/components/onboarding-flow/common/onboarding-container.tsx +++ b/packages/frontend/src/components/onboarding-flow/common/onboarding-container.tsx @@ -27,7 +27,7 @@ interface OnboardingContainerProps { export function OnboardingContainer({ children }: OnboardingContainerProps) { return (
-
{children}
+
{children}
) } diff --git a/packages/frontend/src/components/onboarding-flow/configure-step/configure-step.tsx b/packages/frontend/src/components/onboarding-flow/configure-step/configure-step.tsx index aa022609..d89e4bcc 100644 --- a/packages/frontend/src/components/onboarding-flow/configure-step/configure-step.tsx +++ b/packages/frontend/src/components/onboarding-flow/configure-step/configure-step.tsx @@ -1,4 +1,5 @@ import { useOnboarding } from "@/components/onboarding-flow/store" +import Configure from "@/components/projects/create/Configure" import { FileCog } from "lucide-react" import { useState } from "react" @@ -36,27 +37,31 @@ export function ConfigureStep() { // } return ( -
-
- {/* Header section with icon and description */} -
- -
-

Configure

-

- Set the deployer LRN for a single deployment or by creating a deployer auction for multiple deployments -

+
+
+
+
+ {/* Header section with icon and description */} +
+ +
+

Configure

+

+ Set the deployer LRN for a single deployment or by creating a deployer auction for multiple deployments +

+
+
+ + {/* Content sections will be placed here: + 1. Deployment type tabs (auction/LRN) + 2. Configuration forms + 3. Environment variables + 4. Account selection + + ...content here/ */} +
- - Content sections will be placed here: - 1. Deployment type tabs (auction/LRN) - 2. Configuration forms - 3. Environment variables - 4. Account selection - - ...content here/ - {/* */}
) diff --git a/packages/frontend/src/components/onboarding-flow/connect-step/connect-step.tsx b/packages/frontend/src/components/onboarding-flow/connect-step/connect-step.tsx index 891ecc03..c9b24b60 100644 --- a/packages/frontend/src/components/onboarding-flow/connect-step/connect-step.tsx +++ b/packages/frontend/src/components/onboarding-flow/connect-step/connect-step.tsx @@ -28,20 +28,20 @@ export function ConnectStep() { }; const handleRepositorySelect = (repo: { name: string }) => { - setFormData({ githubRepo: repo.name }); + setFormData({ repoName: repo.name }); nextStep(); }; const handleTemplateSelect = (template: { id: string; name: string }) => { setFormData({ - githubRepo: projectName, - deploymentType: template.id, + repoName: projectName, + framework: template.id, }); nextStep(); }; return ( -
+
{/* \ */} {connectState === 'initial' ? (
diff --git a/packages/frontend/src/components/onboarding-flow/deploy-step/deploy-step.tsx b/packages/frontend/src/components/onboarding-flow/deploy-step/deploy-step.tsx index 4cb6ec38..18593db9 100644 --- a/packages/frontend/src/components/onboarding-flow/deploy-step/deploy-step.tsx +++ b/packages/frontend/src/components/onboarding-flow/deploy-step/deploy-step.tsx @@ -23,25 +23,29 @@ export function DeployStep() { } return ( -
-
- {/* Header section */} -
-
-

Deploy

-

- Your deployment is configured and ready to go! -

+
+
+
+
+ {/* Header section */} +
+
+

Deploy

+

+ Your deployment is configured and ready to go! +

+
+
+ + Content sections will be placed here: + 1. Repository info card + 2. Configuration summary + 3. Deploy button + + {/* ...content here */} +
- - Content sections will be placed here: - 1. Repository info card - 2. Configuration summary - 3. Deploy button - - {/* ...content here */} -
) diff --git a/packages/frontend/src/components/onboarding-flow/index.ts b/packages/frontend/src/components/onboarding-flow/index.ts index 85c40cd9..66f328f3 100644 --- a/packages/frontend/src/components/onboarding-flow/index.ts +++ b/packages/frontend/src/components/onboarding-flow/index.ts @@ -10,6 +10,7 @@ // Main component export { default as Onboarding } from './Onboarding'; +export { default as OnboardingDialog, hasCompletedOnboarding } from './OnboardingDialog'; // Step components export { ConfigureStep } from './configure-step'; diff --git a/packages/frontend/src/components/onboarding-flow/store.ts b/packages/frontend/src/components/onboarding-flow/store.ts index 5b63ae09..eef71e95 100644 --- a/packages/frontend/src/components/onboarding-flow/store.ts +++ b/packages/frontend/src/components/onboarding-flow/store.ts @@ -40,7 +40,14 @@ const STEP_ORDER: Step[] = ['connect', 'configure', 'deploy']; */ export const useOnboarding = create((set) => ({ currentStep: 'connect', - formData: {}, + formData: { + projectName: '', + repoName: '', + repoDescription: '', + framework: '', + access: 'public', + organizationSlug: '', + }, setCurrentStep: (step) => set({ currentStep: step }), setFormData: (data) => set((state) => ({ diff --git a/packages/frontend/src/components/onboarding-flow/store.tsx b/packages/frontend/src/components/onboarding-flow/store.tsx new file mode 100644 index 00000000..ff90f774 --- /dev/null +++ b/packages/frontend/src/components/onboarding-flow/store.tsx @@ -0,0 +1,69 @@ +import { create } from 'zustand'; +import { OnboardingFormData, Step } from './types'; + +// Define the state for the onboarding flow +export interface OnboardingState { + currentStep: Step; + setCurrentStep: (step: Step) => void; + nextStep: () => void; + previousStep: () => void; + formData: OnboardingFormData; + updateFormData: (data: Partial) => void; + resetOnboarding: () => void; +} + +// Create the store with the initial state +export const useOnboarding = create((set) => { + // The steps in order + const STEPS: Step[] = ['connect', 'configure', 'deploy']; + + // Initial form data + const initialFormData: OnboardingFormData = { + projectName: '', + repoName: '', + repoDescription: '', + framework: '', + access: 'public', + organizationSlug: '', + }; + + return { + // Current step state (start with the connect step) + currentStep: 'connect', + + // Function to set the current step + setCurrentStep: (step) => set({ currentStep: step }), + + // Function to move to the next step + nextStep: () => set((state) => { + const currentIndex = STEPS.indexOf(state.currentStep); + if (currentIndex < STEPS.length - 1) { + return { currentStep: STEPS[currentIndex + 1] }; + } + return state; + }), + + // Function to move to the previous step + previousStep: () => set((state) => { + const currentIndex = STEPS.indexOf(state.currentStep); + if (currentIndex > 0) { + return { currentStep: STEPS[currentIndex - 1] }; + } + return state; + }), + + // Form data state + formData: initialFormData, + + // Function to update form data + updateFormData: (data) => set((state) => ({ + formData: { ...state.formData, ...data } + })), + + // Function to reset the onboarding state + resetOnboarding: () => set({ + currentStep: 'connect', + formData: initialFormData + }), + }; +}); \ No newline at end of file diff --git a/packages/frontend/src/components/onboarding-flow/types.ts b/packages/frontend/src/components/onboarding-flow/types.ts index 1378e66f..7aebd66f 100644 --- a/packages/frontend/src/components/onboarding-flow/types.ts +++ b/packages/frontend/src/components/onboarding-flow/types.ts @@ -12,14 +12,20 @@ export type Step = 'connect' | 'configure' | 'deploy'; /** * Form data collected during the onboarding process * @interface OnboardingFormData - * @property {string} [githubRepo] - Selected GitHub repository - * @property {string} [deploymentType] - Selected deployment type (e.g., "pwa") - * @property {Record} [environmentVars] - Environment variables + * @property {string} projectName - Project name + * @property {string} repoName - Repository name + * @property {string} repoDescription - Repository description + * @property {string} framework - Framework used for the project + * @property {string} access - Access level of the repository + * @property {string} organizationSlug - Organization slug */ export interface OnboardingFormData { - githubRepo?: string; - deploymentType?: string; - environmentVars?: Record; + projectName: string; + repoName: string; + repoDescription: string; + framework: string; + access: 'public' | 'private'; + organizationSlug: string; } /** diff --git a/packages/frontend/src/components/projects/create/ConnectAccount.tsx b/packages/frontend/src/components/projects/create/ConnectAccount.tsx index edf30b2c..44ef1ce0 100644 --- a/packages/frontend/src/components/projects/create/ConnectAccount.tsx +++ b/packages/frontend/src/components/projects/create/ConnectAccount.tsx @@ -53,7 +53,7 @@ const ConnectAccount: React.FC = ({ // TODO: Use correct height return ( -
+
{/** Icons */}
diff --git a/packages/frontend/src/pages/index.tsx b/packages/frontend/src/pages/index.tsx index add9a2c2..f5fec7b5 100644 --- a/packages/frontend/src/pages/index.tsx +++ b/packages/frontend/src/pages/index.tsx @@ -2,51 +2,85 @@ import { Organization } from 'gql-client'; import { Loader2 } from 'lucide-react'; import { useCallback, useEffect, useState } from 'react'; import { Navigate, useNavigate } from 'react-router-dom'; +import { OnboardingDialog, hasCompletedOnboarding } from '../components/onboarding-flow'; import { useGQLClient } from '../context/GQLClientContext'; +import { useOctokit } from '../context/OctokitContext'; /** - * Index component that fetches user organizations and redirects to the first organization's slug. - * If no organization is found, it displays a loading spinner. + * Index component that handles post-authentication flow. + * Shows onboarding dialog if needed, then redirects to first organization. * - * @returns {JSX.Element} A JSX element that either navigates to the organization's slug or displays a loading spinner. + * @returns {JSX.Element} The rendered component. */ const Index = () => { const client = useGQLClient(); const [organization, setOrganization] = useState(); + const [loading, setLoading] = useState(true); + const [showOnboarding, setShowOnboarding] = useState(false); const navigate = useNavigate(); + const { octokit } = useOctokit(); + + // Check if GitHub is connected + const isGitHubConnected = Boolean(octokit); /** * Fetches the user's organizations from the GQLClient. * Sets the first organization in the list to the organization state. - * If no organizations are found, navigates to the '/auth' route. + * If no organizations are found, shows onboarding. * * @async * @function fetchUserOrganizations * @returns {Promise} */ const fetchUserOrganizations = useCallback(async () => { - const { organizations } = await client.getOrganizations(); - if (organizations && organizations.length > 0) { - // By default information of first organization displayed - setOrganization(organizations[0]); - } else { - navigate('/auth'); + try { + setLoading(true); + const { organizations } = await client.getOrganizations(); + + if (organizations && organizations.length > 0) { + // By default information of first organization displayed + setOrganization(organizations[0]); + + // Check if onboarding is needed + const onboardingCompleted = hasCompletedOnboarding(); + + // We need onboarding if it hasn't been completed or GitHub is not connected + if (!onboardingCompleted || !isGitHubConnected) { + setShowOnboarding(true); + } + } else { + // No organizations found, show onboarding + setShowOnboarding(true); + } + } catch (error) { + console.error('Error fetching organizations:', error); + // Show onboarding on error + setShowOnboarding(true); + } finally { + setLoading(false); } - }, [client, navigate]); + }, [client, isGitHubConnected]); useEffect(() => { fetchUserOrganizations(); }, [fetchUserOrganizations]); - return ( - <> - {Boolean(organization) ? ( - - ) : ( - - )} - - ); + // Handle onboarding completion + const handleOnboardingClosed = () => { + setShowOnboarding(false); + // Fetch organizations again after onboarding in case new ones were created + fetchUserOrganizations(); + }; + + if (loading) { + return ; + } + + if (showOnboarding) { + return ; + } + + return organization ? : ; }; export default Index;