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';
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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) {
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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';
|
||||
|
@ -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) => ({
|
||||
|
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
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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">
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user