Onboarding opens from with state changes
This commit is contained in:
parent
7fbfdaf85e
commit
aa3fa5d573
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,9 +1,21 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
import { OnboardingDialog } from '@/components/onboarding-flow';
|
||||||
import { Button } from '@/components/ui/button';
|
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 { 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 { LaconicMark } from '@/laconic-assets/laconic-mark';
|
||||||
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
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 { Link, useNavigate } from 'react-router-dom';
|
||||||
import { ProjectSearchBar } from '../search/ProjectSearchBar';
|
import { ProjectSearchBar } from '../search/ProjectSearchBar';
|
||||||
import { ColorModeToggle } from './components/ColorModeToggle';
|
import { ColorModeToggle } from './components/ColorModeToggle';
|
||||||
@ -26,6 +38,58 @@ import { WalletSessionId } from './components/WalletSessionId';
|
|||||||
*/
|
*/
|
||||||
export function TopNavigation() {
|
export function TopNavigation() {
|
||||||
const navigate = useNavigate();
|
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 (
|
return (
|
||||||
<PopoverPrimitive.Root>
|
<PopoverPrimitive.Root>
|
||||||
@ -79,6 +143,28 @@ export function TopNavigation() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
<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 /> */}
|
{/* <NavigationActions /> */}
|
||||||
<Button variant="ghost" asChild className="text-muted-foreground">
|
<Button variant="ghost" asChild className="text-muted-foreground">
|
||||||
<Link to="/support">Support</Link>
|
<Link to="/support">Support</Link>
|
||||||
@ -134,6 +220,14 @@ export function TopNavigation() {
|
|||||||
<Button variant="ghost" asChild className="justify-start">
|
<Button variant="ghost" asChild className="justify-start">
|
||||||
<Link to="/docs">Documentation</Link>
|
<Link to="/docs">Documentation</Link>
|
||||||
</Button>
|
</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>
|
</nav>
|
||||||
<div className="flex items-center justify-between mt-auto">
|
<div className="flex items-center justify-between mt-auto">
|
||||||
<GitHubSessionButton />
|
<GitHubSessionButton />
|
||||||
@ -143,11 +237,37 @@ export function TopNavigation() {
|
|||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
<div className="flex items-center gap-2">
|
<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 />
|
<ColorModeToggle />
|
||||||
<WalletSessionId walletId="0xAb...1234" />
|
<WalletSessionId walletId="0xAb...1234" />
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Onboarding Dialog */}
|
||||||
|
{showOnboarding && (
|
||||||
|
<OnboardingDialog defaultOpen={true} onClose={handleOnboardingClose} />
|
||||||
|
)}
|
||||||
</PopoverPrimitive.Root>
|
</PopoverPrimitive.Root>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -6,15 +6,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
OnboardingContainer,
|
OnboardingContainer, StepNavigation
|
||||||
StepHeader,
|
|
||||||
StepNavigation,
|
|
||||||
} from '@/components/onboarding-flow/common';
|
} from '@/components/onboarding-flow/common';
|
||||||
import { ConfigureStep } from '@/components/onboarding-flow/configure-step';
|
import { ConfigureStep } from '@/components/onboarding-flow/configure-step';
|
||||||
import { ConnectStep } from '@/components/onboarding-flow/connect-step';
|
import { ConnectStep } from '@/components/onboarding-flow/connect-step';
|
||||||
import { DeployStep } from '@/components/onboarding-flow/deploy-step';
|
import { DeployStep } from '@/components/onboarding-flow/deploy-step';
|
||||||
import { SidebarNav } from '@/components/onboarding-flow/sidebar';
|
import { SidebarNav } from '@/components/onboarding-flow/sidebar';
|
||||||
import { useOnboarding } from '@/components/onboarding-flow/store';
|
import { useOnboarding } from '@/components/onboarding-flow/store';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { FileCog, GitPullRequest, SquareArrowOutDownRight } from 'lucide-react';
|
import { FileCog, GitPullRequest, SquareArrowOutDownRight } from 'lucide-react';
|
||||||
|
|
||||||
/** Icons for each step in the onboarding flow */
|
/** Icons for each step in the onboarding flow */
|
||||||
@ -58,17 +57,19 @@ export default function Onboarding() {
|
|||||||
return (
|
return (
|
||||||
<OnboardingContainer>
|
<OnboardingContainer>
|
||||||
<SidebarNav currentStep={currentStep} />
|
<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">
|
<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
|
{/* <StepHeader
|
||||||
icon={stepIcons[currentStep]}
|
icon={stepIcons[currentStep]}
|
||||||
title={stepTitles[currentStep]}
|
title={stepTitles[currentStep]}
|
||||||
description={stepDescriptions[currentStep]}
|
description={stepDescriptions[currentStep]}
|
||||||
/>
|
/> */}
|
||||||
<div className="flex-1 flex items-center justify-center py-8">
|
<div className="py-4 px-1">
|
||||||
{currentStep === 'connect' && <ConnectStep />}
|
<ScrollArea className="flex-1 mt-6 mb-6">
|
||||||
{currentStep === 'configure' && <ConfigureStep />}
|
{currentStep === 'connect' && <ConnectStep />}
|
||||||
{currentStep === 'deploy' && <DeployStep />}
|
{currentStep === 'configure' && <ConfigureStep />}
|
||||||
</div>
|
{currentStep === 'deploy' && <DeployStep />}
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
<StepNavigation
|
<StepNavigation
|
||||||
currentStep={currentStep}
|
currentStep={currentStep}
|
||||||
onPrevious={previousStep}
|
onPrevious={previousStep}
|
||||||
|
@ -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;
|
@ -27,7 +27,7 @@ interface OnboardingContainerProps {
|
|||||||
export function OnboardingContainer({ children }: OnboardingContainerProps) {
|
export function OnboardingContainer({ children }: OnboardingContainerProps) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen w-full bg-background flex items-center justify-center p-8 relative overflow-hidden">
|
<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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { useOnboarding } from "@/components/onboarding-flow/store"
|
import { useOnboarding } from "@/components/onboarding-flow/store"
|
||||||
|
import Configure from "@/components/projects/create/Configure"
|
||||||
import { FileCog } from "lucide-react"
|
import { FileCog } from "lucide-react"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
|
|
||||||
@ -36,27 +37,31 @@ export function ConfigureStep() {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center w-full max-w-[445px] mx-auto">
|
<div className="w-full">
|
||||||
<div className="w-full flex flex-col items-center gap-6">
|
<div className="max-w-2xl mx-auto space-y-8">
|
||||||
{/* Header section with icon and description */}
|
<div className="flex flex-col items-center justify-center w-full max-w-[445px] mx-auto">
|
||||||
<div className="flex flex-col items-center gap-1">
|
<div className="w-full flex flex-col items-center gap-6">
|
||||||
<FileCog className="w-16 h-16 text-foreground" />
|
{/* Header section with icon and description */}
|
||||||
<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">Configure</h2>
|
<FileCog className="w-16 h-16 text-foreground" />
|
||||||
<p className="text-base text-muted-foreground text-center">
|
<div className="flex flex-col items-center gap-1">
|
||||||
Set the deployer LRN for a single deployment or by creating a deployer auction for multiple deployments
|
<h2 className="text-2xl font-bold text-foreground">Configure</h2>
|
||||||
</p>
|
<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>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -28,20 +28,20 @@ export function ConnectStep() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRepositorySelect = (repo: { name: string }) => {
|
const handleRepositorySelect = (repo: { name: string }) => {
|
||||||
setFormData({ githubRepo: repo.name });
|
setFormData({ repoName: repo.name });
|
||||||
nextStep();
|
nextStep();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTemplateSelect = (template: { id: string; name: string }) => {
|
const handleTemplateSelect = (template: { id: string; name: string }) => {
|
||||||
setFormData({
|
setFormData({
|
||||||
githubRepo: projectName,
|
repoName: projectName,
|
||||||
deploymentType: template.id,
|
framework: template.id,
|
||||||
});
|
});
|
||||||
nextStep();
|
nextStep();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-2xl mx-auto">
|
<div className="max-w-2xl w-full">
|
||||||
{/* <ConnectAccountTabPanel />\ */}
|
{/* <ConnectAccountTabPanel />\ */}
|
||||||
{connectState === 'initial' ? (
|
{connectState === 'initial' ? (
|
||||||
<div className="flex flex-col items-center justify-center gap-6 p-8">
|
<div className="flex flex-col items-center justify-center gap-6 p-8">
|
||||||
|
@ -23,25 +23,29 @@ export function DeployStep() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center w-full max-w-[445px] mx-auto">
|
<div className="w-full">
|
||||||
<div className="w-full flex flex-col items-center gap-6">
|
<div className="max-w-2xl mx-auto space-y-8">
|
||||||
{/* Header section */}
|
<div className="flex flex-col items-center justify-center w-full max-w-[445px] mx-auto">
|
||||||
<div className="flex flex-col items-center gap-1">
|
<div className="w-full flex flex-col items-center gap-6">
|
||||||
<div className="flex flex-col items-center gap-1">
|
{/* Header section */}
|
||||||
<h2 className="text-2xl font-bold text-foreground">Deploy</h2>
|
<div className="flex flex-col items-center gap-1">
|
||||||
<p className="text-base text-muted-foreground text-center">
|
<div className="flex flex-col items-center gap-1">
|
||||||
Your deployment is configured and ready to go!
|
<h2 className="text-2xl font-bold text-foreground">Deploy</h2>
|
||||||
</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
Content sections will be placed here:
|
|
||||||
1. Repository info card
|
|
||||||
2. Configuration summary
|
|
||||||
3. Deploy button
|
|
||||||
|
|
||||||
{/* ...content here */}
|
|
||||||
<Deploy/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
// Main component
|
// Main component
|
||||||
export { default as Onboarding } from './Onboarding';
|
export { default as Onboarding } from './Onboarding';
|
||||||
|
export { default as OnboardingDialog, hasCompletedOnboarding } from './OnboardingDialog';
|
||||||
|
|
||||||
// Step components
|
// Step components
|
||||||
export { ConfigureStep } from './configure-step';
|
export { ConfigureStep } from './configure-step';
|
||||||
|
@ -40,7 +40,14 @@ const STEP_ORDER: Step[] = ['connect', 'configure', 'deploy'];
|
|||||||
*/
|
*/
|
||||||
export const useOnboarding = create<OnboardingState>((set) => ({
|
export const useOnboarding = create<OnboardingState>((set) => ({
|
||||||
currentStep: 'connect',
|
currentStep: 'connect',
|
||||||
formData: {},
|
formData: {
|
||||||
|
projectName: '',
|
||||||
|
repoName: '',
|
||||||
|
repoDescription: '',
|
||||||
|
framework: '',
|
||||||
|
access: 'public',
|
||||||
|
organizationSlug: '',
|
||||||
|
},
|
||||||
setCurrentStep: (step) => set({ currentStep: step }),
|
setCurrentStep: (step) => set({ currentStep: step }),
|
||||||
setFormData: (data) =>
|
setFormData: (data) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
|
69
packages/frontend/src/components/onboarding-flow/store.tsx
Normal file
69
packages/frontend/src/components/onboarding-flow/store.tsx
Normal 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
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
@ -12,14 +12,20 @@ export type Step = 'connect' | 'configure' | 'deploy';
|
|||||||
/**
|
/**
|
||||||
* Form data collected during the onboarding process
|
* Form data collected during the onboarding process
|
||||||
* @interface OnboardingFormData
|
* @interface OnboardingFormData
|
||||||
* @property {string} [githubRepo] - Selected GitHub repository
|
* @property {string} projectName - Project name
|
||||||
* @property {string} [deploymentType] - Selected deployment type (e.g., "pwa")
|
* @property {string} repoName - Repository name
|
||||||
* @property {Record<string, string>} [environmentVars] - Environment variables
|
* @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 {
|
export interface OnboardingFormData {
|
||||||
githubRepo?: string;
|
projectName: string;
|
||||||
deploymentType?: string;
|
repoName: string;
|
||||||
environmentVars?: Record<string, string>;
|
repoDescription: string;
|
||||||
|
framework: string;
|
||||||
|
access: 'public' | 'private';
|
||||||
|
organizationSlug: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -53,7 +53,7 @@ const ConnectAccount: React.FC<ConnectAccountInterface> = ({
|
|||||||
|
|
||||||
// TODO: Use correct height
|
// TODO: Use correct height
|
||||||
return (
|
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]">
|
<div className="flex flex-col items-center max-w-[420px]">
|
||||||
{/** Icons */}
|
{/** Icons */}
|
||||||
<div className="w-52 mb-7 inline-flex items-center justify-center h-16 gap-4">
|
<div className="w-52 mb-7 inline-flex items-center justify-center h-16 gap-4">
|
||||||
|
@ -2,51 +2,85 @@ import { Organization } from 'gql-client';
|
|||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { Navigate, useNavigate } from 'react-router-dom';
|
import { Navigate, useNavigate } from 'react-router-dom';
|
||||||
|
import { OnboardingDialog, hasCompletedOnboarding } from '../components/onboarding-flow';
|
||||||
import { useGQLClient } from '../context/GQLClientContext';
|
import { useGQLClient } from '../context/GQLClientContext';
|
||||||
|
import { useOctokit } from '../context/OctokitContext';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Index component that fetches user organizations and redirects to the first organization's slug.
|
* Index component that handles post-authentication flow.
|
||||||
* If no organization is found, it displays a loading spinner.
|
* 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 Index = () => {
|
||||||
const client = useGQLClient();
|
const client = useGQLClient();
|
||||||
const [organization, setOrganization] = useState<Organization>();
|
const [organization, setOrganization] = useState<Organization>();
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [showOnboarding, setShowOnboarding] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { octokit } = useOctokit();
|
||||||
|
|
||||||
|
// Check if GitHub is connected
|
||||||
|
const isGitHubConnected = Boolean(octokit);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the user's organizations from the GQLClient.
|
* Fetches the user's organizations from the GQLClient.
|
||||||
* Sets the first organization in the list to the organization state.
|
* 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
|
* @async
|
||||||
* @function fetchUserOrganizations
|
* @function fetchUserOrganizations
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
const fetchUserOrganizations = useCallback(async () => {
|
const fetchUserOrganizations = useCallback(async () => {
|
||||||
const { organizations } = await client.getOrganizations();
|
try {
|
||||||
if (organizations && organizations.length > 0) {
|
setLoading(true);
|
||||||
// By default information of first organization displayed
|
const { organizations } = await client.getOrganizations();
|
||||||
setOrganization(organizations[0]);
|
|
||||||
} else {
|
if (organizations && organizations.length > 0) {
|
||||||
navigate('/auth');
|
// 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(() => {
|
useEffect(() => {
|
||||||
fetchUserOrganizations();
|
fetchUserOrganizations();
|
||||||
}, [fetchUserOrganizations]);
|
}, [fetchUserOrganizations]);
|
||||||
|
|
||||||
return (
|
// Handle onboarding completion
|
||||||
<>
|
const handleOnboardingClosed = () => {
|
||||||
{Boolean(organization) ? (
|
setShowOnboarding(false);
|
||||||
<Navigate to={organization!.slug} />
|
// Fetch organizations again after onboarding in case new ones were created
|
||||||
) : (
|
fetchUserOrganizations();
|
||||||
<Loader2 className={'animate-spin w-12 h-12'} />
|
};
|
||||||
)}
|
|
||||||
</>
|
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;
|
export default Index;
|
||||||
|
Loading…
Reference in New Issue
Block a user