SEction 9-10
This commit is contained in:
280
frontend/src/components/onboarding/OnboardingWizard.tsx
Normal file
280
frontend/src/components/onboarding/OnboardingWizard.tsx
Normal file
@@ -0,0 +1,280 @@
|
||||
/**
|
||||
* 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 {
|
||||
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: 'Install WordPress plugin', 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}
|
||||
/>
|
||||
);
|
||||
case 2:
|
||||
return (
|
||||
<Step2AddSite
|
||||
data={wizardData}
|
||||
updateData={updateWizardData}
|
||||
onNext={handleNext}
|
||||
onBack={handleBack}
|
||||
setIsLoading={setIsLoading}
|
||||
/>
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<Step3ConnectIntegration
|
||||
data={wizardData}
|
||||
updateData={updateWizardData}
|
||||
onNext={handleNext}
|
||||
onBack={handleBack}
|
||||
onSkip={handleSkipStep}
|
||||
/>
|
||||
);
|
||||
case 4:
|
||||
return (
|
||||
<Step4AddKeywords
|
||||
data={wizardData}
|
||||
updateData={updateWizardData}
|
||||
onNext={handleNext}
|
||||
onBack={handleBack}
|
||||
onSkip={handleSkipStep}
|
||||
/>
|
||||
);
|
||||
case 5:
|
||||
return (
|
||||
<Step5Complete
|
||||
data={wizardData}
|
||||
onComplete={handleComplete}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-2xl bg-white dark:bg-gray-900 rounded-2xl shadow-2xl overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="size-10 rounded-xl bg-gradient-to-br from-brand-500 to-brand-600 flex items-center justify-center text-white">
|
||||
<BoltIcon className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-gray-900 dark:text-white">
|
||||
Getting Started
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Step {currentStep} of {STEPS.length}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleSkipAll}
|
||||
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
>
|
||||
<CloseIcon className="w-5 h-5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="mt-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
{STEPS.map((step) => (
|
||||
<div
|
||||
key={step.id}
|
||||
className={`flex items-center ${step.id < STEPS.length ? 'flex-1' : ''}`}
|
||||
>
|
||||
<div
|
||||
className={`size-8 rounded-full flex items-center justify-center text-sm font-medium transition-colors ${
|
||||
step.id < currentStep
|
||||
? 'bg-green-500 text-white'
|
||||
: step.id === currentStep
|
||||
? 'bg-brand-500 text-white'
|
||||
: 'bg-gray-200 dark:bg-gray-700 text-gray-500 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{step.id < currentStep ? (
|
||||
<CheckCircleIcon className="w-5 h-5" />
|
||||
) : (
|
||||
step.id
|
||||
)}
|
||||
</div>
|
||||
{step.id < STEPS.length && (
|
||||
<div
|
||||
className={`h-1 flex-1 mx-2 rounded transition-colors ${
|
||||
step.id < currentStep
|
||||
? 'bg-green-500'
|
||||
: 'bg-gray-200 dark:bg-gray-700'
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400">
|
||||
{STEPS.map((step) => (
|
||||
<span
|
||||
key={step.id}
|
||||
className={`text-center ${
|
||||
step.id === currentStep ? 'text-brand-600 dark:text-brand-400 font-medium' : ''
|
||||
}`}
|
||||
style={{ width: `${100 / STEPS.length}%` }}
|
||||
>
|
||||
{step.title}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Step Content */}
|
||||
<div className="p-6">
|
||||
{renderStepContent()}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user