Onboarding opens from with state changes

This commit is contained in:
icld 2025-02-27 13:15:01 -08:00
parent 7fbfdaf85e
commit aa3fa5d573
15 changed files with 603 additions and 172 deletions

View File

@ -1,17 +0,0 @@
import type React from "react"
import "@/styles/globals.css"
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className="bg-zinc-900 text-white">
<div className="flex min-h-screen">{children}</div>
</body>
</html>
)
}

View File

@ -1,74 +0,0 @@
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: <GitPullRequest className="h-6 w-6 stroke-2" />,
configure: <FileCog className="h-6 w-6 stroke-2" />,
deploy: <SquareArrowOutDownRight className="h-6 w-6 stroke-2" />,
};
/** 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 (
<OnboardingContainer>
<SidebarNav currentStep={currentStep} />
<div className="flex-1 bg-primary-foreground rounded-lg p-8 shadow-[0_1px_2px_0_rgba(0,0,0,0.06),0_1px_3px_0_rgba(0,0,0,0.1)] flex flex-col">
<StepHeader
icon={stepIcons[currentStep]}
title={stepTitles[currentStep]}
description={stepDescriptions[currentStep]}
/>
<div className="flex-1 flex items-center justify-center py-8">
{currentStep === 'connect' && <ConnectStep />}
{currentStep === 'configure' && <ConfigureStep />}
{currentStep === 'deploy' && <DeployStep />}
</div>
<StepNavigation
currentStep={currentStep}
onPrevious={previousStep}
onNext={nextStep}
nextLabel={currentStep === 'deploy' ? 'Deploy' : 'Next'}
/>
</div>
</OnboardingContainer>
);
}

View File

@ -1,9 +1,21 @@
'use client';
import { OnboardingDialog } from '@/components/onboarding-flow';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
import { useGQLClient } from '@/context/GQLClientContext';
import { useOctokit } from '@/context/OctokitContext';
import { LaconicMark } from '@/laconic-assets/laconic-mark';
import * as PopoverPrimitive from '@radix-ui/react-popover';
import { Menu, Shapes, Wallet } from 'lucide-react';
import { Organization } from 'gql-client';
import { Menu, Plus, Shapes, Wallet } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { ProjectSearchBar } from '../search/ProjectSearchBar';
import { ColorModeToggle } from './components/ColorModeToggle';
@ -26,6 +38,58 @@ import { WalletSessionId } from './components/WalletSessionId';
*/
export function TopNavigation() {
const navigate = useNavigate();
const [showOnboarding, setShowOnboarding] = useState(false);
const { octokit } = useOctokit();
const client = useGQLClient();
const [defaultOrg, setDefaultOrg] = useState<Organization | null>(null);
// Check if GitHub is connected
const isGitHubConnected = Boolean(octokit);
// Fetch the default organization (first one in the list)
const fetchDefaultOrganization = useCallback(async () => {
try {
const { organizations } = await client.getOrganizations();
if (organizations && organizations.length > 0) {
setDefaultOrg(organizations[0]);
}
} catch (error) {
console.error('Error fetching organizations:', error);
}
}, [client]);
useEffect(() => {
if (isGitHubConnected) {
fetchDefaultOrganization();
}
}, [isGitHubConnected, fetchDefaultOrganization]);
const handleOnboardingClose = () => {
setShowOnboarding(false);
// Refresh organization data after onboarding
fetchDefaultOrganization();
};
// Navigate to create page with organization slug
const handleCreateNew = () => {
if (defaultOrg) {
navigate(`/${defaultOrg.slug}/projects/create`);
} else {
// If no organization is available, show onboarding
setShowOnboarding(true);
}
};
const handleRunOnboarding = () => {
// Clear existing onboarding progress data
localStorage.removeItem('onboarding_progress');
localStorage.removeItem('onboarding_state');
// Force starting from connect step
localStorage.setItem('onboarding_force_connect', 'true');
setShowOnboarding(true);
};
return (
<PopoverPrimitive.Root>
@ -79,6 +143,28 @@ export function TopNavigation() {
</div>
<div className="flex items-center gap-4">
{/* Add New Button with Dropdown */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="rounded-full">
<Plus className="h-5 w-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{isGitHubConnected && (
<>
<DropdownMenuItem onClick={handleCreateNew}>
Create New
</DropdownMenuItem>
<DropdownMenuSeparator />
</>
)}
<DropdownMenuItem onClick={handleRunOnboarding}>
Run Onboarding
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{/* <NavigationActions /> */}
<Button variant="ghost" asChild className="text-muted-foreground">
<Link to="/support">Support</Link>
@ -134,6 +220,14 @@ export function TopNavigation() {
<Button variant="ghost" asChild className="justify-start">
<Link to="/docs">Documentation</Link>
</Button>
<Button variant="ghost" className="justify-start" onClick={handleRunOnboarding}>
Run Onboarding
</Button>
{isGitHubConnected && (
<Button variant="ghost" className="justify-start" onClick={handleCreateNew}>
Create New
</Button>
)}
</nav>
<div className="flex items-center justify-between mt-auto">
<GitHubSessionButton />
@ -143,11 +237,37 @@ export function TopNavigation() {
</SheetContent>
</Sheet>
<div className="flex items-center gap-2">
{/* Add New Button (Mobile) */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="rounded-full">
<Plus className="h-5 w-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{isGitHubConnected && (
<>
<DropdownMenuItem onClick={handleCreateNew}>
Create New
</DropdownMenuItem>
<DropdownMenuSeparator />
</>
)}
<DropdownMenuItem onClick={handleRunOnboarding}>
Run Onboarding
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<ColorModeToggle />
<WalletSessionId walletId="0xAb...1234" />
</div>
</nav>
</div>
{/* Onboarding Dialog */}
{showOnboarding && (
<OnboardingDialog defaultOpen={true} onClose={handleOnboardingClose} />
)}
</PopoverPrimitive.Root>
);
}

View File

@ -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 (
<OnboardingContainer>
<SidebarNav currentStep={currentStep} />
<div className="flex-1 bg-primary-foreground rounded-lg p-8 shadow-[0_1px_2px_0_rgba(0,0,0,0.06),0_1px_3px_0_rgba(0,0,0,0.1)] flex flex-col">
<StepHeader
<div className="flex-1 bg-primary-foreground rounded-lg p-8 shadow-[0_1px_2px_0_rgba(0,0,0,0.06),0_1px_3px_0_rgba(0,0,0,0.1)] flex flex-col overflow-hidden">
{/* <StepHeader
icon={stepIcons[currentStep]}
title={stepTitles[currentStep]}
description={stepDescriptions[currentStep]}
/>
<div className="flex-1 flex items-center justify-center py-8">
{currentStep === 'connect' && <ConnectStep />}
{currentStep === 'configure' && <ConfigureStep />}
{currentStep === 'deploy' && <DeployStep />}
</div>
/> */}
<div className="py-4 px-1">
<ScrollArea className="flex-1 mt-6 mb-6">
{currentStep === 'connect' && <ConnectStep />}
{currentStep === 'configure' && <ConfigureStep />}
{currentStep === 'deploy' && <DeployStep />}
</ScrollArea>
</div>
<StepNavigation
currentStep={currentStep}
onPrevious={previousStep}

View File

@ -0,0 +1,275 @@
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { useOctokit } from '@/context/OctokitContext';
import React, { useEffect, useState } from 'react';
import Onboarding from './Onboarding';
import { useOnboarding } from './store';
import { OnboardingFormData, Step } from "./types";
// Local storage keys
const ONBOARDING_COMPLETED_KEY = 'onboarding_completed';
const ONBOARDING_STATE_KEY = 'onboarding_state';
const ONBOARDING_PROGRESS_KEY = 'onboarding_progress';
const ONBOARDING_FORCE_CONNECT_KEY = 'onboarding_force_connect';
interface OnboardingDialogProps {
trigger?: React.ReactNode;
defaultOpen?: boolean;
onClose?: () => 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<OnboardingDialogProps> = ({
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<OnboardingFormData>);
}
}
} 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 (
<>
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
{trigger && <DialogTrigger asChild>{trigger}</DialogTrigger>}
<DialogContent className="max-w-[95vw] max-h-[95vh] w-[1200px] h-[800px] overflow-hidden p-0">
<div className="h-full overflow-hidden">
<Onboarding />
</div>
</DialogContent>
</Dialog>
{/* Exit Warning Dialog */}
<AlertDialog open={showExitWarning} onOpenChange={setShowExitWarning}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Exit Onboarding?</AlertDialogTitle>
<AlertDialogDescription>
You haven't completed the onboarding process. If you exit now, your progress will be lost, including any organization or repository selections.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={cancelClose}>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={completeClose}>Exit Anyway</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* Continue Progress Dialog */}
<AlertDialog open={showContinueAlert} onOpenChange={setShowContinueAlert}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Continue Onboarding?</AlertDialogTitle>
<AlertDialogDescription>
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?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={startFresh}>Start Fresh</AlertDialogCancel>
<AlertDialogAction onClick={continueOnboarding}>Continue</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
};
/**
* 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;

View File

@ -27,7 +27,7 @@ interface OnboardingContainerProps {
export function OnboardingContainer({ children }: OnboardingContainerProps) {
return (
<div className="min-h-screen w-full bg-background flex items-center justify-center p-8 relative overflow-hidden">
<div className="flex gap-6 w-full max-w-[1200px] min-h-[700px] relative z-10">{children}</div>
<div className="flex gap-6 w-full max-w-[1200px] min-h-[700px] h-full relative z-10 overflow-hidden">{children}</div>
</div>
)
}

View File

@ -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 (
<div className="flex flex-col items-center justify-center w-full max-w-[445px] mx-auto">
<div className="w-full flex flex-col items-center gap-6">
{/* Header section with icon and description */}
<div className="flex flex-col items-center gap-1">
<FileCog className="w-16 h-16 text-foreground" />
<div className="flex flex-col items-center gap-1">
<h2 className="text-2xl font-bold text-foreground">Configure</h2>
<p className="text-base text-muted-foreground text-center">
Set the deployer LRN for a single deployment or by creating a deployer auction for multiple deployments
</p>
<div className="w-full">
<div className="max-w-2xl mx-auto space-y-8">
<div className="flex flex-col items-center justify-center w-full max-w-[445px] mx-auto">
<div className="w-full flex flex-col items-center gap-6">
{/* Header section with icon and description */}
<div className="flex flex-col items-center gap-1">
<FileCog className="w-16 h-16 text-foreground" />
<div className="flex flex-col items-center gap-1">
<h2 className="text-2xl font-bold text-foreground">Configure</h2>
<p className="text-base text-muted-foreground text-center">
Set the deployer LRN for a single deployment or by creating a deployer auction for multiple deployments
</p>
</div>
</div>
{/* Content sections will be placed here:
1. Deployment type tabs (auction/LRN)
2. Configuration forms
3. Environment variables
4. Account selection
...content here/ */}
<Configure/>
</div>
</div>
Content sections will be placed here:
1. Deployment type tabs (auction/LRN)
2. Configuration forms
3. Environment variables
4. Account selection
...content here/
{/* <Configure/> */}
</div>
</div>
)

View File

@ -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 (
<div className="max-w-2xl mx-auto">
<div className="max-w-2xl w-full">
{/* <ConnectAccountTabPanel />\ */}
{connectState === 'initial' ? (
<div className="flex flex-col items-center justify-center gap-6 p-8">

View File

@ -23,25 +23,29 @@ export function DeployStep() {
}
return (
<div className="flex flex-col items-center justify-center w-full max-w-[445px] mx-auto">
<div className="w-full flex flex-col items-center gap-6">
{/* Header section */}
<div className="flex flex-col items-center gap-1">
<div className="flex flex-col items-center gap-1">
<h2 className="text-2xl font-bold text-foreground">Deploy</h2>
<p className="text-base text-muted-foreground text-center">
Your deployment is configured and ready to go!
</p>
<div className="w-full">
<div className="max-w-2xl mx-auto space-y-8">
<div className="flex flex-col items-center justify-center w-full max-w-[445px] mx-auto">
<div className="w-full flex flex-col items-center gap-6">
{/* Header section */}
<div className="flex flex-col items-center gap-1">
<div className="flex flex-col items-center gap-1">
<h2 className="text-2xl font-bold text-foreground">Deploy</h2>
<p className="text-base text-muted-foreground text-center">
Your deployment is configured and ready to go!
</p>
</div>
</div>
Content sections will be placed here:
1. Repository info card
2. Configuration summary
3. Deploy button
{/* ...content here */}
<Deploy/>
</div>
</div>
Content sections will be placed here:
1. Repository info card
2. Configuration summary
3. Deploy button
{/* ...content here */}
<Deploy/>
</div>
</div>
)

View File

@ -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';

View File

@ -40,7 +40,14 @@ const STEP_ORDER: Step[] = ['connect', 'configure', 'deploy'];
*/
export const useOnboarding = create<OnboardingState>((set) => ({
currentStep: 'connect',
formData: {},
formData: {
projectName: '',
repoName: '',
repoDescription: '',
framework: '',
access: 'public',
organizationSlug: '',
},
setCurrentStep: (step) => set({ currentStep: step }),
setFormData: (data) =>
set((state) => ({

View File

@ -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<OnboardingFormData>) => void;
resetOnboarding: () => void;
}
// Create the store with the initial state
export const useOnboarding = create<OnboardingState>((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
}),
};
});

View File

@ -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<string, string>} [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<string, string>;
projectName: string;
repoName: string;
repoDescription: string;
framework: string;
access: 'public' | 'private';
organizationSlug: string;
}
/**

View File

@ -53,7 +53,7 @@ const ConnectAccount: React.FC<ConnectAccountInterface> = ({
// TODO: Use correct height
return (
<div className="dark:bg-overlay gap-7 rounded-2xl flex flex-col items-center justify-center h-full p-4 text-sm text-center bg-gray-100">
<div className="dark:bg-overlay gap-7 rounded-2xl flex flex-col items-center justify-center p-4 text-sm text-center bg-gray-100">
<div className="flex flex-col items-center max-w-[420px]">
{/** Icons */}
<div className="w-52 mb-7 inline-flex items-center justify-center h-16 gap-4">

View File

@ -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<Organization>();
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<void>}
*/
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) ? (
<Navigate to={organization!.slug} />
) : (
<Loader2 className={'animate-spin w-12 h-12'} />
)}
</>
);
// Handle onboarding completion
const handleOnboardingClosed = () => {
setShowOnboarding(false);
// Fetch organizations again after onboarding in case new ones were created
fetchUserOrganizations();
};
if (loading) {
return <Loader2 className="animate-spin w-12 h-12" />;
}
if (showOnboarding) {
return <OnboardingDialog defaultOpen={true} onClose={handleOnboardingClosed} />;
}
return organization ? <Navigate to={organization.slug} /> : <Loader2 className="animate-spin w-12 h-12" />;
};
export default Index;