263 lines
7.8 KiB
TypeScript
263 lines
7.8 KiB
TypeScript
/**
|
|
* OnboardingWizard Component
|
|
* Multi-step wizard for new user onboarding
|
|
* Provides a guided flow: Welcome → Add Site → Connect Integration → Add Keywords → Complete
|
|
*/
|
|
import React, { useState, useCallback, useEffect } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { Card } from '../ui/card';
|
|
import Button from '../ui/button/Button';
|
|
import IconButton from '../ui/button/IconButton';
|
|
import {
|
|
ArrowRightIcon,
|
|
ArrowLeftIcon,
|
|
CloseIcon,
|
|
BoltIcon,
|
|
CheckCircleIcon,
|
|
} from '../../icons';
|
|
import { useOnboardingStore } from '../../store/onboardingStore';
|
|
import { useToast } from '../ui/toast/ToastContainer';
|
|
|
|
// Step components
|
|
import Step1Welcome from './steps/Step1Welcome';
|
|
import Step2AddSite from './steps/Step2AddSite';
|
|
import Step3ConnectIntegration from './steps/Step3ConnectIntegration';
|
|
import Step4AddKeywords from './steps/Step4AddKeywords';
|
|
import Step5Complete from './steps/Step5Complete';
|
|
|
|
export interface WizardStep {
|
|
id: number;
|
|
title: string;
|
|
description: string;
|
|
isOptional?: boolean;
|
|
}
|
|
|
|
export interface WizardData {
|
|
// Site data
|
|
siteName: string;
|
|
domain: string;
|
|
hostingType: string;
|
|
industryId: number | null;
|
|
industrySlug: string;
|
|
selectedSectors: string[];
|
|
// Created site reference
|
|
createdSiteId: number | null;
|
|
// Integration data
|
|
integrationTested: boolean;
|
|
// Keywords data
|
|
keywordsAdded: boolean;
|
|
keywordsCount: number;
|
|
}
|
|
|
|
const STEPS: WizardStep[] = [
|
|
{ id: 1, title: 'Welcome', description: 'Get started with IGNY8' },
|
|
{ id: 2, title: 'Add Site', description: 'Create your first site' },
|
|
{ id: 3, title: 'Connect', description: 'Connect your site', isOptional: true },
|
|
{ id: 4, title: 'Keywords', description: 'Add target keywords', isOptional: true },
|
|
{ id: 5, title: 'Complete', description: 'You\'re all set!' },
|
|
];
|
|
|
|
interface OnboardingWizardProps {
|
|
onComplete?: () => void;
|
|
onSkip?: () => void;
|
|
}
|
|
|
|
export default function OnboardingWizard({ onComplete, onSkip }: OnboardingWizardProps) {
|
|
const navigate = useNavigate();
|
|
const toast = useToast();
|
|
const { dismissGuide } = useOnboardingStore();
|
|
|
|
const [currentStep, setCurrentStep] = useState(1);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
const [wizardData, setWizardData] = useState<WizardData>({
|
|
siteName: '',
|
|
domain: '',
|
|
hostingType: 'wordpress',
|
|
industryId: null,
|
|
industrySlug: '',
|
|
selectedSectors: [],
|
|
createdSiteId: null,
|
|
integrationTested: false,
|
|
keywordsAdded: false,
|
|
keywordsCount: 0,
|
|
});
|
|
|
|
const updateWizardData = useCallback((updates: Partial<WizardData>) => {
|
|
setWizardData(prev => ({ ...prev, ...updates }));
|
|
}, []);
|
|
|
|
const handleNext = useCallback(() => {
|
|
if (currentStep < STEPS.length) {
|
|
setCurrentStep(prev => prev + 1);
|
|
}
|
|
}, [currentStep]);
|
|
|
|
const handleBack = useCallback(() => {
|
|
if (currentStep > 1) {
|
|
setCurrentStep(prev => prev - 1);
|
|
}
|
|
}, [currentStep]);
|
|
|
|
const handleSkipStep = useCallback(() => {
|
|
// Skip to next step (for optional steps)
|
|
handleNext();
|
|
}, [handleNext]);
|
|
|
|
const handleComplete = useCallback(async () => {
|
|
setIsLoading(true);
|
|
try {
|
|
await dismissGuide();
|
|
|
|
if (onComplete) {
|
|
onComplete();
|
|
}
|
|
|
|
// Navigate to the site dashboard
|
|
if (wizardData.createdSiteId) {
|
|
navigate(`/sites/${wizardData.createdSiteId}`);
|
|
} else {
|
|
navigate('/dashboard');
|
|
}
|
|
|
|
toast.success('Welcome to IGNY8! Your content pipeline is ready.');
|
|
} catch (error) {
|
|
console.error('Failed to complete onboarding:', error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, [dismissGuide, navigate, onComplete, toast, wizardData.createdSiteId]);
|
|
|
|
const handleSkipAll = useCallback(async () => {
|
|
await dismissGuide();
|
|
if (onSkip) {
|
|
onSkip();
|
|
}
|
|
navigate('/dashboard');
|
|
}, [dismissGuide, navigate, onSkip]);
|
|
|
|
// Calculate progress percentage
|
|
const progressPercent = ((currentStep - 1) / (STEPS.length - 1)) * 100;
|
|
|
|
const renderStepContent = () => {
|
|
switch (currentStep) {
|
|
case 1:
|
|
return (
|
|
<Step1Welcome
|
|
onNext={handleNext}
|
|
onSkip={handleSkipAll}
|
|
currentStep={currentStep}
|
|
totalSteps={STEPS.length}
|
|
/>
|
|
);
|
|
case 2:
|
|
return (
|
|
<Step2AddSite
|
|
data={wizardData}
|
|
updateData={updateWizardData}
|
|
onNext={handleNext}
|
|
onBack={handleBack}
|
|
setIsLoading={setIsLoading}
|
|
currentStep={currentStep}
|
|
totalSteps={STEPS.length}
|
|
/>
|
|
);
|
|
case 3:
|
|
return (
|
|
<Step3ConnectIntegration
|
|
data={wizardData}
|
|
updateData={updateWizardData}
|
|
onNext={handleNext}
|
|
onBack={handleBack}
|
|
onSkip={handleSkipStep}
|
|
currentStep={currentStep}
|
|
totalSteps={STEPS.length}
|
|
/>
|
|
);
|
|
case 4:
|
|
return (
|
|
<Step4AddKeywords
|
|
data={wizardData}
|
|
updateData={updateWizardData}
|
|
onNext={handleNext}
|
|
onBack={handleBack}
|
|
onSkip={handleSkipStep}
|
|
currentStep={currentStep}
|
|
totalSteps={STEPS.length}
|
|
/>
|
|
);
|
|
case 5:
|
|
return (
|
|
<Step5Complete
|
|
data={wizardData}
|
|
onComplete={handleComplete}
|
|
isLoading={isLoading}
|
|
currentStep={currentStep}
|
|
totalSteps={STEPS.length}
|
|
/>
|
|
);
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="w-full max-w-6xl mx-auto">
|
|
{/* Progress Steps - Clean horizontal stepper */}
|
|
<div className="mb-8">
|
|
<div className="flex items-center justify-between">
|
|
{STEPS.map((step, index) => (
|
|
<React.Fragment key={step.id}>
|
|
<div className="flex flex-col items-center">
|
|
<div
|
|
className={`size-12 rounded-full flex items-center justify-center text-base font-semibold transition-all ${
|
|
step.id < currentStep
|
|
? 'bg-success-500 text-white shadow-md'
|
|
: step.id === currentStep
|
|
? 'bg-brand-500 text-white shadow-lg ring-4 ring-brand-100 dark:ring-brand-900/50'
|
|
: 'bg-gray-100 dark:bg-gray-800 text-gray-400 dark:text-gray-500'
|
|
}`}
|
|
>
|
|
{step.id < currentStep ? (
|
|
<CheckCircleIcon className="w-6 h-6" />
|
|
) : (
|
|
step.id
|
|
)}
|
|
</div>
|
|
<span
|
|
className={`mt-2 text-sm font-medium text-center ${
|
|
step.id === currentStep
|
|
? 'text-brand-600 dark:text-brand-400'
|
|
: step.id < currentStep
|
|
? 'text-success-600 dark:text-success-400'
|
|
: 'text-gray-400 dark:text-gray-500'
|
|
}`}
|
|
>
|
|
{step.title}
|
|
</span>
|
|
</div>
|
|
{index < STEPS.length - 1 && (
|
|
<div
|
|
className={`flex-1 h-1.5 mx-4 rounded-full transition-colors ${
|
|
step.id < currentStep
|
|
? 'bg-success-500'
|
|
: 'bg-gray-200 dark:bg-gray-700'
|
|
}`}
|
|
/>
|
|
)}
|
|
</React.Fragment>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Step Content Card */}
|
|
<Card className="w-full bg-white dark:bg-gray-900 rounded-2xl shadow-xl overflow-hidden border border-gray-200 dark:border-gray-800">
|
|
{/* Step Content */}
|
|
<div className="p-8 sm:p-10">
|
|
{renderStepContent()}
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|