Phase 2, 2.1 and 2.2 complete

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-05 08:17:56 +00:00
parent abc6c011ea
commit cb8e747387
14 changed files with 1834 additions and 552 deletions

View File

@@ -21,11 +21,11 @@ const ComponentCard: React.FC<ComponentCardProps> = ({
{(title || desc || headerContent) && (
<div className="px-6 py-5 relative z-0 flex items-start justify-between gap-4">
<div className="flex-1">
<h3 className="text-base font-medium text-gray-800 dark:text-white/90">
<h3 className="text-sm font-semibold uppercase tracking-wide text-gray-600 dark:text-gray-400">
{title}
</h3>
{desc && (
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
<p className="mt-1 text-base text-gray-800 dark:text-gray-200">
{desc}
</p>
)}

View File

@@ -0,0 +1,208 @@
/**
* Account Info Widget
* Displays account-related billing information:
* - Credits consumed/remaining this billing period
* - Reset day (when credits refresh)
* - Last payment date
* - Next payment due date
* - Current plan/package type
*/
import React from 'react';
import { useNavigate } from 'react-router-dom';
import ComponentCard from '../common/ComponentCard';
import Button from '../ui/button/Button';
import Badge from '../ui/badge/Badge';
import { CreditBalance, Subscription, Plan } from '../../services/billing.api';
import {
CalendarIcon,
CreditCardIcon,
UserCircleIcon,
ArrowRightIcon,
} from '../../icons';
interface AccountInfoWidgetProps {
balance: CreditBalance | null;
subscription?: Subscription | null;
plan?: Plan | null;
userPlan?: any; // Plan from user.account.plan
loading?: boolean;
}
// Helper to format dates
function formatDate(dateStr: string | undefined): string {
if (!dateStr) return '—';
try {
const date = new Date(dateStr);
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
});
} catch {
return '—';
}
}
// Helper to get days until date
function getDaysUntil(dateStr: string | undefined): number | null {
if (!dateStr) return null;
try {
const date = new Date(dateStr);
const now = new Date();
const diff = date.getTime() - now.getTime();
return Math.ceil(diff / (1000 * 60 * 60 * 24));
} catch {
return null;
}
}
export default function AccountInfoWidget({
balance,
subscription,
plan,
userPlan,
loading = false,
}: AccountInfoWidgetProps) {
const navigate = useNavigate();
if (loading) {
return (
<ComponentCard title="Account Info" desc="Billing & subscription">
<div className="animate-pulse space-y-4">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="h-12 bg-gray-100 dark:bg-gray-800 rounded-lg" />
))}
</div>
</ComponentCard>
);
}
const periodEnd = subscription?.current_period_end;
const daysUntilReset = getDaysUntil(periodEnd);
// Resolve plan - prefer explicit plan, then subscription plan, then userPlan
const currentPlan = plan || (subscription?.plan && typeof subscription.plan === 'object' ? subscription.plan : null) || userPlan;
return (
<ComponentCard
title="Account Info"
desc="Billing & subscription"
headerContent={
<Button
variant="ghost"
size="sm"
onClick={() => navigate('/account/settings')}
endIcon={<ArrowRightIcon className="w-3 h-3" />}
>
Manage
</Button>
}
>
<div className="space-y-4">
{/* Current Plan */}
<div className="flex items-center justify-between p-4 rounded-xl bg-brand-50 dark:bg-brand-900/20 border border-brand-100 dark:border-brand-800">
<div className="flex items-center gap-3">
<div className="size-10 rounded-lg bg-brand-100 dark:bg-brand-900/50 flex items-center justify-center text-brand-600 dark:text-brand-400">
<UserCircleIcon className="w-5 h-5" />
</div>
<div>
<p className="text-xs text-brand-600 dark:text-brand-400 uppercase font-medium tracking-wide">
Current Plan
</p>
<p className="text-lg font-semibold text-brand-900 dark:text-brand-100">
{currentPlan?.name || 'Free Plan'}
</p>
</div>
</div>
{subscription?.status && (
<Badge
variant="soft"
color={subscription.status === 'active' ? 'success' : 'warning'}
size="sm"
>
{subscription.status}
</Badge>
)}
</div>
{/* Info Grid */}
<div className="grid grid-cols-2 gap-3">
{/* Credits This Period */}
<div className="p-4 rounded-xl bg-gray-50 dark:bg-gray-800/50 border border-gray-100 dark:border-gray-700">
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">
Used This Period
</p>
<p className="text-xl font-bold text-gray-900 dark:text-white">
{balance?.credits_used_this_month?.toLocaleString() || '0'}
</p>
<p className="text-sm text-gray-400 dark:text-gray-500">
of {balance?.plan_credits_per_month?.toLocaleString() || '0'} credits
</p>
</div>
{/* Credits Remaining */}
<div className="p-4 rounded-xl bg-gray-50 dark:bg-gray-800/50 border border-gray-100 dark:border-gray-700">
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">
Remaining
</p>
<p className="text-xl font-bold text-success-600 dark:text-success-400">
{balance?.credits_remaining?.toLocaleString() || '0'}
</p>
<p className="text-sm text-gray-400 dark:text-gray-500">
credits available
</p>
</div>
{/* Reset Date */}
<div className="p-4 rounded-xl bg-gray-50 dark:bg-gray-800/50 border border-gray-100 dark:border-gray-700">
<div className="flex items-center gap-2 mb-1">
<CalendarIcon className="w-4 h-4 text-gray-400" />
<p className="text-sm text-gray-500 dark:text-gray-400">
Credits Reset
</p>
</div>
<p className="text-base font-semibold text-gray-900 dark:text-white">
{formatDate(periodEnd)}
</p>
{daysUntilReset !== null && daysUntilReset > 0 && (
<p className="text-sm text-gray-400 dark:text-gray-500">
in {daysUntilReset} day{daysUntilReset !== 1 ? 's' : ''}
</p>
)}
</div>
{/* Next Payment */}
<div className="p-4 rounded-xl bg-gray-50 dark:bg-gray-800/50 border border-gray-100 dark:border-gray-700">
<div className="flex items-center gap-2 mb-1">
<CreditCardIcon className="w-4 h-4 text-gray-400" />
<p className="text-sm text-gray-500 dark:text-gray-400">
Next Payment
</p>
</div>
<p className="text-base font-semibold text-gray-900 dark:text-white">
{formatDate(periodEnd)}
</p>
{currentPlan?.price && (
<p className="text-sm text-gray-400 dark:text-gray-500">
${currentPlan.price}/mo
</p>
)}
</div>
</div>
{/* Upgrade CTA (if on free or lower plan) */}
{(!currentPlan || currentPlan.slug === 'free' || currentPlan.slug === 'starter') && (
<Button
variant="primary"
tone="brand"
size="sm"
onClick={() => navigate('/account/upgrade')}
className="w-full"
>
Upgrade Plan
</Button>
)}
</div>
</ComponentCard>
);
}

View File

@@ -0,0 +1,134 @@
/**
* Credits Usage Widget
* Displays remaining credits, AI runs count, and visual progress indicator
*/
import React from 'react';
import { useNavigate } from 'react-router-dom';
import ComponentCard from '../common/ComponentCard';
import Button from '../ui/button/Button';
import { CreditBalance } from '../../services/billing.api';
import { BoltIcon, PlusIcon } from '../../icons';
interface CreditsUsageWidgetProps {
balance: CreditBalance | null;
aiOperations?: {
total: number;
period: string;
};
loading?: boolean;
}
export default function CreditsUsageWidget({
balance,
aiOperations,
loading = false,
}: CreditsUsageWidgetProps) {
const navigate = useNavigate();
if (loading || !balance) {
return (
<ComponentCard title="Credits Usage" desc="Your content allowance">
<div className="animate-pulse space-y-4">
<div className="h-20 bg-gray-100 dark:bg-gray-800 rounded-lg" />
<div className="h-4 bg-gray-100 dark:bg-gray-800 rounded w-3/4" />
</div>
</ComponentCard>
);
}
const usagePercentage = balance.plan_credits_per_month > 0
? (balance.credits_used_this_month / balance.plan_credits_per_month) * 100
: 0;
const remainingPercentage = 100 - usagePercentage;
// Determine color based on remaining credits
const getProgressColor = () => {
if (remainingPercentage > 50) return 'bg-success-500';
if (remainingPercentage > 25) return 'bg-warning-500';
return 'bg-error-500';
};
return (
<ComponentCard
title="Credits Usage"
desc="Your content allowance"
headerContent={
<Button
variant="ghost"
size="sm"
onClick={() => navigate('/account/credits')}
startIcon={<PlusIcon className="w-4 h-4" />}
>
Buy More
</Button>
}
>
<div className="space-y-5">
{/* Main Credit Display */}
<div className="flex items-center justify-between">
<div>
<p className="text-base text-gray-500 dark:text-gray-400 mb-1">
Available Credits
</p>
<p className="text-4xl font-bold text-gray-900 dark:text-white">
{balance.credits.toLocaleString()}
</p>
</div>
<div className={`size-16 rounded-xl flex items-center justify-center ${
remainingPercentage > 50
? 'bg-success-100 dark:bg-success-900/30 text-success-600 dark:text-success-400'
: remainingPercentage > 25
? 'bg-warning-100 dark:bg-warning-900/30 text-warning-600 dark:text-warning-400'
: 'bg-error-100 dark:bg-error-900/30 text-error-600 dark:text-error-400'
}`}>
<BoltIcon className="w-8 h-8" />
</div>
</div>
{/* Usage Progress Bar */}
<div>
<div className="flex justify-between items-center mb-2 text-base">
<span className="text-gray-600 dark:text-gray-400">Monthly Usage</span>
<span className="font-semibold text-gray-900 dark:text-white">
{balance.credits_used_this_month.toLocaleString()} / {balance.plan_credits_per_month.toLocaleString()}
</span>
</div>
<div className="w-full bg-gray-100 dark:bg-gray-800 rounded-full h-3 overflow-hidden">
<div
className={`h-full rounded-full transition-all duration-500 ${getProgressColor()}`}
style={{ width: `${Math.min(usagePercentage, 100)}%` }}
/>
</div>
<div className="flex justify-between items-center mt-2 text-sm text-gray-500 dark:text-gray-400">
<span>{usagePercentage.toFixed(1)}% used</span>
<span>{balance.credits_remaining.toLocaleString()} remaining</span>
</div>
</div>
{/* AI Operations Summary */}
{aiOperations && (
<div className="pt-4 border-t border-gray-100 dark:border-gray-800">
<div className="flex items-center justify-between">
<div>
<p className="text-base text-gray-500 dark:text-gray-400">
AI Operations ({aiOperations.period})
</p>
<p className="text-2xl font-bold text-gray-900 dark:text-white">
{aiOperations.total.toLocaleString()}
</p>
</div>
<Button
variant="outline"
size="md"
onClick={() => navigate('/account/usage')}
>
View Details
</Button>
</div>
</div>
)}
</div>
</ComponentCard>
);
}

View File

@@ -0,0 +1,171 @@
/**
* Sites Overview Widget
* Compact display of sites with status, stats, and quick action buttons
*/
import React from 'react';
import { useNavigate } from 'react-router-dom';
import ComponentCard from '../common/ComponentCard';
import Button from '../ui/button/Button';
import IconButton from '../ui/button/IconButton';
import Badge from '../ui/badge/Badge';
import { Site } from '../../services/api';
import {
SettingsIcon,
EyeIcon,
FileIcon,
PlusIcon,
CheckCircleIcon,
} from '../../icons';
interface SitesOverviewWidgetProps {
sites: Site[];
loading?: boolean;
onAddSite?: () => void;
maxSites?: number;
}
export default function SitesOverviewWidget({
sites,
loading = false,
onAddSite,
maxSites = 0,
}: SitesOverviewWidgetProps) {
const navigate = useNavigate();
const canAddMoreSites = maxSites === 0 || sites.length < maxSites;
if (loading) {
return (
<ComponentCard title="Sites Overview" desc="Your connected sites">
<div className="animate-pulse space-y-3">
{[1, 2].map((i) => (
<div key={i} className="h-16 bg-gray-100 dark:bg-gray-800 rounded-lg" />
))}
</div>
</ComponentCard>
);
}
return (
<ComponentCard
title="Sites Overview"
desc={`${sites.length} site${sites.length !== 1 ? 's' : ''} connected`}
headerContent={
canAddMoreSites && onAddSite && (
<Button
variant="ghost"
size="sm"
onClick={onAddSite}
startIcon={<PlusIcon className="w-4 h-4" />}
>
Add Site
</Button>
)
}
>
{sites.length === 0 ? (
<div className="text-center py-8">
<p className="text-gray-500 dark:text-gray-400 mb-4">
No sites configured yet
</p>
{onAddSite && (
<Button
variant="primary"
onClick={onAddSite}
startIcon={<PlusIcon className="w-4 h-4" />}
>
Add Your First Site
</Button>
)}
</div>
) : (
<div className="space-y-3">
{sites.slice(0, 5).map((site) => (
<div
key={site.id}
className="flex items-center justify-between p-3 rounded-lg bg-gray-50 dark:bg-gray-800/50 border border-gray-100 dark:border-gray-700 hover:border-brand-200 dark:hover:border-brand-800 transition-colors"
>
<div className="flex items-center gap-3 min-w-0 flex-1">
{/* Status Indicator */}
<div className={`size-2.5 rounded-full flex-shrink-0 ${
site.is_active ? 'bg-success-500' : 'bg-gray-300 dark:bg-gray-600'
}`} />
{/* Site Info */}
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<h4 className="font-medium text-gray-900 dark:text-white truncate">
{site.name}
</h4>
<Badge
variant="soft"
color={site.is_active ? 'success' : 'neutral'}
size="xs"
>
{site.is_active ? 'Active' : 'Inactive'}
</Badge>
</div>
{site.domain && (
<p className="text-xs text-gray-500 dark:text-gray-400 truncate">
{site.domain}
</p>
)}
</div>
{/* Quick Stats */}
<div className="hidden sm:flex items-center gap-4 text-xs text-gray-500 dark:text-gray-400">
{site.active_sectors_count !== undefined && (
<span title="Active sectors">
{site.active_sectors_count} sectors
</span>
)}
{(site as any).page_count !== undefined && (
<span title="Content pages">
{(site as any).page_count} pages
</span>
)}
</div>
</div>
{/* Action Buttons */}
<div className="flex items-center gap-1 ml-2">
<IconButton
variant="ghost"
size="sm"
onClick={() => navigate(`/sites/${site.id}`)}
icon={<EyeIcon className="w-4 h-4" />}
aria-label="View Dashboard"
/>
<IconButton
variant="ghost"
size="sm"
onClick={() => navigate(`/sites/${site.id}/content`)}
icon={<FileIcon className="w-4 h-4" />}
aria-label="View Content"
/>
<IconButton
variant="ghost"
size="sm"
onClick={() => navigate(`/sites/${site.id}/settings`)}
icon={<SettingsIcon className="w-4 h-4" />}
aria-label="Site Settings"
/>
</div>
</div>
))}
{sites.length > 5 && (
<Button
variant="ghost"
tone="neutral"
size="sm"
onClick={() => navigate('/sites')}
className="w-full"
>
View all {sites.length} sites
</Button>
)}
</div>
)}
</ComponentCard>
);
}

View File

@@ -55,12 +55,12 @@ const stages = [
{ key: 'published', label: 'Published', icon: PaperPlaneIcon, href: '/writer/published', gradient: 'from-success-500 to-success-600' },
] as const;
// Small filled arrow triangle component
// Filled arrow triangle component - positioned at end of flex item
function ArrowTip() {
return (
<div className="flex items-center justify-center w-4 h-4 mx-1">
<svg viewBox="0 0 8 12" className="w-2.5 h-3.5 fill-brand-500 dark:fill-brand-400">
<path d="M0 0 L8 6 L0 12 Z" />
<div className="flex items-center justify-center w-6 h-6">
<svg viewBox="0 0 10 16" className="w-4 h-5 fill-brand-500 dark:fill-brand-400">
<path d="M0 0 L10 8 L0 16 Z" />
</svg>
</div>
);
@@ -71,26 +71,32 @@ export default function WorkflowPipelineWidget({ data, loading }: WorkflowPipeli
<div className="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-800 p-5">
{/* Header */}
<div className="flex items-center justify-between mb-5">
<h3 className="text-base font-semibold text-gray-800 dark:text-gray-200 uppercase tracking-wide">
Workflow Pipeline
</h3>
<div>
<h3 className="text-sm font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wide">
Workflow Pipeline
</h3>
<p className="text-base text-gray-800 dark:text-gray-200 mt-0.5">
Content creation progress
</p>
</div>
<span className="text-3xl font-bold text-brand-600 dark:text-brand-400">
{data.completionPercentage}%
</span>
</div>
{/* Pipeline Flow - Single Balanced Row */}
<div className="flex items-center justify-between mb-5">
<div className="flex items-center mb-5">
{stages.map((stage, index) => {
const Icon = stage.icon;
const count = data[stage.key as keyof PipelineData];
const isTransparent = 'transparent' in stage && stage.transparent;
const isLast = index === stages.length - 1;
return (
<div key={stage.key} className="flex items-center">
<div key={stage.key} className="flex items-center flex-1">
<Link
to={stage.href}
className="flex flex-col items-center group min-w-[60px]"
className="flex flex-col items-center group flex-1"
>
<div className={`p-2.5 rounded-xl ${
isTransparent
@@ -106,7 +112,11 @@ export default function WorkflowPipelineWidget({ data, loading }: WorkflowPipeli
{loading ? '—' : typeof count === 'number' ? count.toLocaleString() : count}
</span>
</Link>
{index < stages.length - 1 && <ArrowTip />}
{!isLast && (
<div className="flex-shrink-0 mx-1">
<ArrowTip />
</div>
)}
</div>
);
})}

View File

@@ -52,7 +52,7 @@ export interface WizardData {
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: 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!' },
];
@@ -146,6 +146,8 @@ export default function OnboardingWizard({ onComplete, onSkip }: OnboardingWizar
<Step1Welcome
onNext={handleNext}
onSkip={handleSkipAll}
currentStep={currentStep}
totalSteps={STEPS.length}
/>
);
case 2:
@@ -156,6 +158,8 @@ export default function OnboardingWizard({ onComplete, onSkip }: OnboardingWizar
onNext={handleNext}
onBack={handleBack}
setIsLoading={setIsLoading}
currentStep={currentStep}
totalSteps={STEPS.length}
/>
);
case 3:
@@ -166,6 +170,8 @@ export default function OnboardingWizard({ onComplete, onSkip }: OnboardingWizar
onNext={handleNext}
onBack={handleBack}
onSkip={handleSkipStep}
currentStep={currentStep}
totalSteps={STEPS.length}
/>
);
case 4:
@@ -176,6 +182,8 @@ export default function OnboardingWizard({ onComplete, onSkip }: OnboardingWizar
onNext={handleNext}
onBack={handleBack}
onSkip={handleSkipStep}
currentStep={currentStep}
totalSteps={STEPS.length}
/>
);
case 5:
@@ -184,6 +192,8 @@ export default function OnboardingWizard({ onComplete, onSkip }: OnboardingWizar
data={wizardData}
onComplete={handleComplete}
isLoading={isLoading}
currentStep={currentStep}
totalSteps={STEPS.length}
/>
);
default:
@@ -192,86 +202,58 @@ export default function OnboardingWizard({ onComplete, onSkip }: OnboardingWizar
};
return (
<div className="w-full max-w-3xl mx-auto">
<Card className="w-full bg-white dark:bg-gray-900 rounded-2xl shadow-lg 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>
<IconButton
variant="ghost"
size="sm"
onClick={handleSkipAll}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
icon={<CloseIcon className="w-5 h-5" />}
/>
</div>
{/* Progress Bar */}
<div className="mt-4">
<div className="flex items-center justify-between mb-2">
{STEPS.map((step) => (
<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
key={step.id}
className={`flex items-center ${step.id < STEPS.length ? 'flex-1' : ''}`}
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'
}`}
>
<div
className={`size-8 rounded-full flex items-center justify-center text-sm font-medium transition-colors ${
step.id < currentStep
? 'bg-success-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-success-500'
: 'bg-gray-200 dark:bg-gray-700'
}`}
/>
{step.id < currentStep ? (
<CheckCircleIcon className="w-6 h-6" />
) : (
step.id
)}
</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' : ''
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'
}`}
style={{ width: `${100 / STEPS.length}%` }}
>
{step.title}
</span>
))}
</div>
</div>
</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-6">
<div className="p-8 sm:p-10">
{renderStepContent()}
</div>
</Card>

View File

@@ -1,6 +1,6 @@
/**
* Step 1: Welcome
* Introduction screen for new users
* Redesigned intro with cleaner cards explaining what each step accomplishes
*/
import React from 'react';
import Button from '../../ui/button/Button';
@@ -9,99 +9,126 @@ import {
BoltIcon,
FileTextIcon,
PlugInIcon,
PieChartIcon,
CheckCircleIcon,
} from '../../../icons';
interface Step1WelcomeProps {
onNext: () => void;
onSkip: () => void;
currentStep: number;
totalSteps: number;
}
const FEATURES = [
const WIZARD_STEPS = [
{
icon: <FileTextIcon className="h-5 w-5" />,
title: 'AI Content Creation',
description: 'Generate high-quality articles with AI assistance',
step: 1,
icon: <FileTextIcon className="h-6 w-6" />,
title: 'Add Your Site',
description: 'Set up your first site with industry preferences',
outcome: 'A configured site ready for content generation',
color: 'brand',
},
{
icon: <PlugInIcon className="h-5 w-5" />,
title: 'WordPress Integration',
description: 'Publish directly to your WordPress site',
step: 2,
icon: <PlugInIcon className="h-6 w-6" />,
title: 'Connect Site',
description: 'Install our plugin to enable direct publishing',
outcome: 'One-click publishing to your site',
color: 'success',
optional: true,
},
{
icon: <BoltIcon className="h-5 w-5" />,
title: 'Automated Pipeline',
description: 'Set it and forget it content scheduling',
},
{
icon: <PieChartIcon className="h-5 w-5" />,
title: 'Smart Analytics',
description: 'Track content performance and optimize',
step: 3,
icon: <BoltIcon className="h-6 w-6" />,
title: 'Add Keywords',
description: 'Define target keywords for AI content',
outcome: 'Keywords ready for clustering and ideas',
color: 'warning',
optional: true,
},
];
export default function Step1Welcome({ onNext, onSkip }: Step1WelcomeProps) {
export default function Step1Welcome({ onNext, onSkip, currentStep, totalSteps }: Step1WelcomeProps) {
return (
<div className="text-center">
<div>
{/* Hero Section */}
<div className="mb-8">
<div className="inline-flex items-center justify-center size-20 rounded-2xl bg-gradient-to-br from-brand-500 to-brand-600 text-white mb-4 shadow-lg">
<BoltIcon className="h-10 w-10" />
</div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
<div className="text-center mb-10">
<h1 className="text-3xl sm:text-4xl font-bold text-gray-900 dark:text-white mb-4">
Welcome to IGNY8
</h1>
<p className="text-gray-600 dark:text-gray-400 max-w-md mx-auto">
Your complete AI-powered content creation and publishing platform.
Let's get you set up in just a few minutes.
<p className="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
Let's set up your AI-powered content pipeline in just a few steps.
You'll be creating SEO-optimized content in minutes.
</p>
</div>
{/* Features Grid */}
<div className="grid grid-cols-2 gap-4 mb-8">
{FEATURES.map((feature, index) => (
<div
key={index}
className="p-4 rounded-xl bg-gray-50 dark:bg-gray-800 text-left"
>
<div className="size-10 rounded-lg bg-brand-100 dark:bg-brand-900/50 text-brand-600 dark:text-brand-400 flex items-center justify-center mb-3">
{feature.icon}
{/* What You'll Accomplish - Horizontal Cards */}
<div className="mb-10">
<h2 className="text-base font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-5 text-center">
What we'll set up together
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{WIZARD_STEPS.map((item) => (
<div
key={item.step}
className="flex flex-col p-5 rounded-xl bg-gray-50 dark:bg-gray-800/50 border border-gray-100 dark:border-gray-700"
>
<div className="flex items-center gap-3 mb-3">
<div className={`size-12 rounded-lg flex items-center justify-center flex-shrink-0 ${
item.color === 'brand'
? 'bg-brand-100 dark:bg-brand-900/50 text-brand-600 dark:text-brand-400'
: item.color === 'success'
? 'bg-success-100 dark:bg-success-900/50 text-success-600 dark:text-success-400'
: 'bg-warning-100 dark:bg-warning-900/50 text-warning-600 dark:text-warning-400'
}`}>
{item.icon}
</div>
{item.optional && (
<span className="text-xs px-2 py-1 rounded-full bg-gray-200 dark:bg-gray-700 text-gray-500 dark:text-gray-400">
Optional
</span>
)}
</div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
{item.title}
</h3>
<p className="text-base text-gray-500 dark:text-gray-400 mb-3 flex-1">
{item.description}
</p>
<div className="flex items-center gap-2 text-sm text-success-600 dark:text-success-400">
<CheckCircleIcon className="w-4 h-4 flex-shrink-0" />
<span>{item.outcome}</span>
</div>
</div>
<h3 className="font-semibold text-gray-900 dark:text-white text-sm mb-1">
{feature.title}
</h3>
<p className="text-xs text-gray-500 dark:text-gray-400">
{feature.description}
</p>
</div>
))}
))}
</div>
</div>
{/* What's Next */}
<div className="bg-brand-50 dark:bg-brand-900/20 rounded-xl p-4 mb-6">
<h3 className="font-semibold text-brand-900 dark:text-brand-100 mb-2">
What we'll do together:
</h3>
<ul className="text-sm text-brand-700 dark:text-brand-300 space-y-1">
<li>✓ Create your first site with optimized defaults</li>
<li>✓ Connect your WordPress installation</li>
<li>✓ Add keywords to start your content pipeline</li>
</ul>
{/* Time Estimate */}
<div className="text-center mb-8 p-4 rounded-xl bg-brand-50 dark:bg-brand-900/20 border border-brand-100 dark:border-brand-800">
<p className="text-base text-brand-700 dark:text-brand-300">
<span className="font-semibold">⏱️ Estimated time:</span> 3-5 minutes
</p>
</div>
{/* Actions */}
<div className="flex items-center justify-between">
<div className="flex items-center justify-between pt-6 border-t border-gray-200 dark:border-gray-700">
<Button
variant="ghost"
tone="neutral"
size="md"
onClick={onSkip}
>
Skip for now
</Button>
<span className="text-sm text-gray-500 dark:text-gray-400">
Step <span className="font-semibold text-gray-700 dark:text-gray-300">{currentStep}</span> of <span className="font-semibold text-gray-700 dark:text-gray-300">{totalSteps}</span>
</span>
<Button
variant="primary"
size="md"
onClick={onNext}
endIcon={<ArrowRightIcon className="w-4 h-4" />}
endIcon={<ArrowRightIcon className="w-5 h-5" />}
>
Let's Get Started
</Button>

View File

@@ -14,6 +14,8 @@ import {
PageIcon,
GridIcon,
CheckCircleIcon,
LockIcon,
PlusIcon,
} from '../../../icons';
import {
fetchIndustries,
@@ -32,6 +34,8 @@ interface Step2AddSiteProps {
onNext: () => void;
onBack: () => void;
setIsLoading: (loading: boolean) => void;
currentStep: number;
totalSteps: number;
}
export default function Step2AddSite({
@@ -39,7 +43,9 @@ export default function Step2AddSite({
updateData,
onNext,
onBack,
setIsLoading
setIsLoading,
currentStep,
totalSteps,
}: Step2AddSiteProps) {
const toast = useToast();
@@ -49,6 +55,9 @@ export default function Step2AddSite({
const [isCreating, setIsCreating] = useState(false);
const [error, setError] = useState<string | null>(null);
// Check if site is already created (locked state)
const isSiteCreated = !!data.createdSiteId;
// Load industries on mount
useEffect(() => {
const loadIndustries = async () => {
@@ -56,6 +65,12 @@ export default function Step2AddSite({
setLoadingIndustries(true);
const response = await fetchIndustries();
setIndustries(response.industries || []);
// Restore selected industry if data has industrySlug
if (data.industrySlug && response.industries) {
const industry = response.industries.find((i: Industry) => i.slug === data.industrySlug);
if (industry) setSelectedIndustry(industry);
}
} catch (err: any) {
if (err?.status !== 429) {
console.error('Failed to load industries:', err);
@@ -65,7 +80,7 @@ export default function Step2AddSite({
}
};
loadIndustries();
}, []);
}, [data.industrySlug]);
// Get available sectors for selected industry
const availableSectors = useMemo(() => {
@@ -74,6 +89,7 @@ export default function Step2AddSite({
}, [selectedIndustry]);
const handleIndustrySelect = (industry: Industry) => {
if (isSiteCreated) return; // Don't allow changes if site is created
setSelectedIndustry(industry);
updateData({
industryId: industry.id || null,
@@ -83,6 +99,7 @@ export default function Step2AddSite({
};
const handleSectorToggle = (sectorSlug: string) => {
if (isSiteCreated) return; // Don't allow changes if site is created
const newSectors = data.selectedSectors.includes(sectorSlug)
? data.selectedSectors.filter(s => s !== sectorSlug)
: [...data.selectedSectors, sectorSlug];
@@ -139,10 +156,33 @@ export default function Step2AddSite({
// Update wizard data with created site
updateData({ createdSiteId: newSite.id });
toast.success(`Site "${data.siteName.trim()}" created successfully!`);
toast.success(`Site "${data.siteName.trim()}" added successfully!`);
onNext();
} catch (err: any) {
setError(err.message || 'Failed to create site');
// Parse error message to show user-friendly text
let errorMessage = 'Failed to add site. Please try again.';
if (err.message) {
const msg = err.message.toLowerCase();
if (msg.includes('name') && (msg.includes('exist') || msg.includes('unique') || msg.includes('duplicate'))) {
errorMessage = 'A site with this name already exists. Please choose a different name.';
} else if (msg.includes('domain') && (msg.includes('exist') || msg.includes('unique') || msg.includes('duplicate'))) {
errorMessage = 'A site with this domain already exists. Please use a different URL.';
} else if (msg.includes('industry')) {
errorMessage = 'Please select a valid industry.';
} else if (msg.includes('sector')) {
errorMessage = 'Please select at least one valid sector.';
} else if (msg.includes('permission') || msg.includes('unauthorized')) {
errorMessage = 'You do not have permission to create sites. Please contact support.';
} else if (msg.includes('limit') || msg.includes('quota') || msg.includes('maximum')) {
errorMessage = 'You have reached the maximum number of sites allowed on your plan.';
} else if (!msg.includes('internal') && !msg.includes('500') && msg.length < 200) {
// Use the original message if it's not an internal error and is reasonable length
errorMessage = err.message;
}
}
setError(errorMessage);
} finally {
setIsCreating(false);
setIsLoading(false);
@@ -151,11 +191,11 @@ export default function Step2AddSite({
return (
<div>
<div className="mb-6">
<h2 className="text-xl font-bold text-gray-900 dark:text-white mb-2">
<div className="mb-8">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
Add Your First Site
</h2>
<p className="text-gray-600 dark:text-gray-400">
<p className="text-base text-gray-600 dark:text-gray-400">
We'll set up your site with optimized defaults for automated content publishing.
</p>
</div>
@@ -168,77 +208,103 @@ export default function Step2AddSite({
/>
)}
{/* Site Name */}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Site Name *
</label>
<input
type="text"
value={data.siteName}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => updateData({ siteName: e.target.value })}
placeholder="My Awesome Blog"
className="w-full px-4 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-transparent"
{/* Site Added Lock Notice */}
{isSiteCreated && (
<Alert
variant="success"
title="Site Added"
message={`Your site "${data.siteName}" has been added. To edit site details, go to Site Settings after completing the wizard.`}
className="mb-8"
/>
</div>
)}
{/* Website URL */}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Website URL (optional)
</label>
<div className="relative">
<PageIcon className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
{/* Form Fields - 3 in a row */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
{/* Site Name */}
<div>
<label className="block text-base font-medium text-gray-700 dark:text-gray-300 mb-2">
Site Name *
</label>
<input
type="text"
value={data.domain}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => updateData({ domain: e.target.value })}
placeholder="https://mysite.com"
className="w-full pl-10 pr-4 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-transparent"
value={data.siteName}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => updateData({ siteName: e.target.value })}
placeholder="My Awesome Blog"
disabled={isSiteCreated}
className={`w-full px-4 py-3 text-base border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-transparent ${isSiteCreated ? 'opacity-60 cursor-not-allowed' : ''}`}
/>
</div>
</div>
{/* Industry Selection */}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Industry *
</label>
{loadingIndustries ? (
<div className="text-sm text-gray-500">Loading industries...</div>
) : (
<SelectDropdown
options={industries.map(i => ({ value: i.slug, label: i.name }))}
value={selectedIndustry?.slug || ''}
onChange={(value) => {
const industry = industries.find(i => i.slug === value);
if (industry) handleIndustrySelect(industry);
}}
placeholder="Select an industry"
/>
)}
{/* Website URL */}
<div>
<label className="block text-base font-medium text-gray-700 dark:text-gray-300 mb-2">
Website URL
</label>
<div className="relative">
<PageIcon className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
type="text"
value={data.domain}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => updateData({ domain: e.target.value })}
placeholder="https://mysite.com"
disabled={isSiteCreated}
className={`w-full pl-11 pr-4 py-3 text-base border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-transparent ${isSiteCreated ? 'opacity-60 cursor-not-allowed' : ''}`}
/>
</div>
</div>
{/* Industry Selection */}
<div>
<label className="block text-base font-medium text-gray-700 dark:text-gray-300 mb-2">
Industry *
</label>
{loadingIndustries ? (
<div className="text-base text-gray-500 py-3">Loading...</div>
) : (
<SelectDropdown
options={industries.map(i => ({ value: i.slug, label: i.name }))}
value={selectedIndustry?.slug || ''}
onChange={(value) => {
const industry = industries.find(i => i.slug === value);
if (industry) handleIndustrySelect(industry);
}}
placeholder="Select industry"
disabled={isSiteCreated}
className="text-base [&_button]:py-3 [&_button]:text-base"
/>
)}
</div>
</div>
{/* Sector Selection */}
{selectedIndustry && availableSectors.length > 0 && (
<div className="mb-6">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
<label className="block text-base font-medium text-gray-700 dark:text-gray-300 mb-3">
Sectors * <span className="text-gray-400 font-normal">(Select up to 5)</span>
{isSiteCreated && data.selectedSectors.length < 5 && (
<span className="text-brand-500 font-normal ml-2">
<PlusIcon className="w-4 h-4 inline-block mr-1" />
You can add more sectors
</span>
)}
</label>
<div className="flex flex-wrap gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg max-h-40 overflow-y-auto">
<div className="flex flex-wrap gap-2 p-4 bg-gray-50 dark:bg-gray-800 rounded-xl max-h-44 overflow-y-auto">
{availableSectors.map((sector: Sector) => {
const isSelected = data.selectedSectors.includes(sector.slug);
// After site creation: can only add new sectors, not remove existing ones
const canToggle = !isSiteCreated || (!isSelected && data.selectedSectors.length < 5);
return (
<Badge
key={sector.slug}
tone={isSelected ? 'success' : 'neutral'}
variant="soft"
className={`cursor-pointer transition-all ${
isSelected ? 'ring-2 ring-success-500' : 'hover:bg-gray-200 dark:hover:bg-gray-700'
}`}
size="md"
className={`transition-all text-base px-4 py-2 ${
isSelected ? 'ring-2 ring-success-500' : ''
} ${canToggle ? 'cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700' : 'opacity-60 cursor-not-allowed'}`}
>
<span onClick={() => handleSectorToggle(sector.slug)} className="flex items-center">
{isSelected && <CheckCircleIcon className="w-3 h-3 mr-1" />}
<span onClick={() => canToggle && handleSectorToggle(sector.slug)} className="flex items-center gap-1.5">
{isSelected && <CheckCircleIcon className="w-4 h-4" />}
{sector.name}
</span>
</Badge>
@@ -246,28 +312,43 @@ export default function Step2AddSite({
})}
</div>
{data.selectedSectors.length > 0 && (
<p className="text-xs text-gray-500 mt-1">
<p className="text-sm text-gray-500 mt-2">
{data.selectedSectors.length} sector{data.selectedSectors.length !== 1 ? 's' : ''} selected
{isSiteCreated && data.selectedSectors.length < 5 && (
<span className="text-brand-500 ml-2">• Click unselected sectors to add more</span>
)}
</p>
)}
</div>
)}
{/* Defaults Info */}
<Card className="p-4 bg-brand-50 dark:bg-brand-900/20 border-brand-200 dark:border-brand-800 mb-6">
<div className="flex items-start gap-3">
<GridIcon className="w-5 h-5 text-brand-600 dark:text-brand-400 mt-0.5" />
<div>
<h4 className="font-medium text-brand-900 dark:text-brand-100 text-sm mb-1">
{/* Defaults Info - 2 columns */}
<Card className="p-5 bg-brand-50 dark:bg-brand-900/20 border-brand-200 dark:border-brand-800 mb-6">
<div className="flex items-start gap-4">
<GridIcon className="w-6 h-6 text-brand-600 dark:text-brand-400 mt-0.5 flex-shrink-0" />
<div className="flex-1">
<h4 className="font-semibold text-brand-900 dark:text-brand-100 text-base mb-3">
Optimized Defaults Applied
</h4>
<ul className="text-xs text-brand-700 dark:text-brand-300 space-y-0.5">
<li>• Auto-approval enabled</li>
<li>• Auto-publish to site enabled</li>
<li>• 3 articles/day limit</li>
<li>• Publishing Mon-Fri at 9am, 2pm, 6pm</li>
</ul>
<p className="text-xs text-brand-600 dark:text-brand-400 mt-2">
<div className="grid grid-cols-2 gap-x-8 gap-y-2 text-base text-brand-700 dark:text-brand-300">
<div className="flex items-center gap-2">
<CheckCircleIcon className="w-4 h-4 text-brand-500" />
<span>Auto-approval enabled</span>
</div>
<div className="flex items-center gap-2">
<CheckCircleIcon className="w-4 h-4 text-brand-500" />
<span>3 articles/day limit</span>
</div>
<div className="flex items-center gap-2">
<CheckCircleIcon className="w-4 h-4 text-brand-500" />
<span>Auto-publish to site</span>
</div>
<div className="flex items-center gap-2">
<CheckCircleIcon className="w-4 h-4 text-brand-500" />
<span>Mon-Fri at 9am, 2pm, 6pm</span>
</div>
</div>
<p className="text-sm text-brand-600 dark:text-brand-400 mt-3">
You can customize these in Site Settings anytime.
</p>
</div>
@@ -275,23 +356,39 @@ export default function Step2AddSite({
</Card>
{/* Actions */}
<div className="flex items-center justify-between">
<div className="flex items-center justify-between pt-4 border-t border-gray-200 dark:border-gray-700">
<Button
variant="ghost"
tone="neutral"
size="md"
onClick={onBack}
startIcon={<ArrowLeftIcon className="w-4 h-4" />}
startIcon={<ArrowLeftIcon className="w-5 h-5" />}
>
Back
</Button>
<Button
variant="primary"
onClick={handleCreateSite}
disabled={isCreating || !data.siteName.trim() || !selectedIndustry || data.selectedSectors.length === 0}
endIcon={!isCreating ? <ArrowRightIcon className="w-4 h-4" /> : undefined}
>
{isCreating ? 'Creating...' : 'Create Site'}
</Button>
<span className="text-sm text-gray-500 dark:text-gray-400">
Step <span className="font-semibold text-gray-700 dark:text-gray-300">{currentStep}</span> of <span className="font-semibold text-gray-700 dark:text-gray-300">{totalSteps}</span>
</span>
{isSiteCreated ? (
<Button
variant="primary"
size="md"
onClick={onNext}
endIcon={<ArrowRightIcon className="w-5 h-5" />}
>
Next
</Button>
) : (
<Button
variant="primary"
size="md"
onClick={handleCreateSite}
disabled={isCreating || !data.siteName.trim() || !selectedIndustry || data.selectedSectors.length === 0}
endIcon={!isCreating ? <ArrowRightIcon className="w-5 h-5" /> : undefined}
>
{isCreating ? 'Adding...' : 'Add Site'}
</Button>
)}
</div>
</div>
);

View File

@@ -1,6 +1,7 @@
/**
* Step 3: Connect Integration
* WordPress plugin installation and connection test
* Platform selection and connection setup
* Supports WordPress, Shopify (coming soon), and Custom (coming soon)
*/
import React, { useState, useEffect } from 'react';
import Button from '../../ui/button/Button';
@@ -15,6 +16,7 @@ import {
CheckCircleIcon,
TimeIcon,
DownloadIcon,
BoxCubeIcon,
} from '../../../icons';
import { integrationApi } from '../../../services/integration.api';
import { useToast } from '../../ui/toast/ToastContainer';
@@ -26,17 +28,50 @@ interface Step3ConnectIntegrationProps {
onNext: () => void;
onBack: () => void;
onSkip: () => void;
currentStep: number;
totalSteps: number;
}
type PlatformType = 'wordpress' | 'shopify' | 'custom';
const PLATFORMS = [
{
id: 'wordpress' as PlatformType,
name: 'WordPress',
description: 'Connect your WordPress site with our plugin',
icon: '🔌',
available: true,
},
{
id: 'shopify' as PlatformType,
name: 'Shopify',
description: 'E-commerce content integration',
icon: '🛒',
available: false,
comingSoon: true,
},
{
id: 'custom' as PlatformType,
name: 'Custom / API',
description: 'Connect via REST API',
icon: '⚡',
available: false,
comingSoon: true,
},
];
export default function Step3ConnectIntegration({
data,
updateData,
onNext,
onBack,
onSkip
onSkip,
currentStep,
totalSteps,
}: Step3ConnectIntegrationProps) {
const toast = useToast();
const [selectedPlatform, setSelectedPlatform] = useState<PlatformType | null>(null);
const [apiKey, setApiKey] = useState<string>('');
const [isTesting, setIsTesting] = useState(false);
const [testResult, setTestResult] = useState<'success' | 'failed' | null>(null);
@@ -82,7 +117,6 @@ export default function Step3ConnectIntegration({
setTestResult(null);
try {
// First get the WordPress integration ID
const integrations = await integrationApi.getSiteIntegrations(data.createdSiteId);
const wpIntegration = integrations.find((i) => i.platform === 'wordpress');
@@ -119,7 +153,7 @@ export default function Step3ConnectIntegration({
variant="outline"
tone="brand"
size="sm"
startIcon={<DownloadIcon className="w-3 h-3" />}
startIcon={<DownloadIcon className="w-4 h-4" />}
>
Download Plugin
</Button>
@@ -135,11 +169,6 @@ export default function Step3ConnectIntegration({
title: 'Activate & Configure',
description: 'Activate the plugin and enter your API key',
},
{
step: 4,
title: 'Test Connection',
description: 'Click the button below to verify the connection',
},
];
if (isLoading) {
@@ -152,143 +181,185 @@ export default function Step3ConnectIntegration({
return (
<div>
<div className="mb-6">
<div className="flex items-center gap-2 mb-2">
<h2 className="text-xl font-bold text-gray-900 dark:text-white">
Connect WordPress
<div className="mb-8">
<div className="flex items-center gap-3 mb-3">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
Connect Your Site
</h2>
<Badge tone="neutral" variant="soft">Optional</Badge>
<Badge tone="neutral" variant="soft" size="sm">Optional</Badge>
</div>
<p className="text-gray-600 dark:text-gray-400">
Install our WordPress plugin to enable automatic publishing. You can do this later if you prefer.
<p className="text-base text-gray-600 dark:text-gray-400">
Select your platform to enable automatic publishing. You can do this later if you prefer.
</p>
</div>
{/* API Key Section */}
{apiKey && (
<Card className="p-4 mb-6 bg-gray-50 dark:bg-gray-800">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Your API Key
</label>
<div className="flex items-center gap-2">
<code className="flex-1 px-3 py-2 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg text-sm font-mono truncate">
{apiKey}
</code>
<Button
variant="outline"
size="sm"
onClick={handleCopyApiKey}
className="flex-shrink-0"
startIcon={<CopyIcon className="w-4 h-4" />}
>
Copy
</Button>
</div>
</Card>
)}
{/* Installation Steps */}
<div className="space-y-3 mb-6">
{INSTALLATION_STEPS.map((item) => (
<div
key={item.step}
className="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg"
{/* Platform Selection */}
<div className="grid grid-cols-3 gap-4 mb-8">
{PLATFORMS.map((platform) => (
<button
key={platform.id}
onClick={() => platform.available && setSelectedPlatform(platform.id)}
disabled={!platform.available}
className={`relative p-5 rounded-xl border-2 text-left transition-all ${
selectedPlatform === platform.id
? 'border-brand-500 bg-brand-50 dark:bg-brand-900/20'
: platform.available
? 'border-gray-200 dark:border-gray-700 hover:border-brand-300 dark:hover:border-brand-700 bg-white dark:bg-gray-800'
: 'border-gray-100 dark:border-gray-800 bg-gray-50 dark:bg-gray-900 opacity-60 cursor-not-allowed'
}`}
>
<div className="size-6 rounded-full bg-brand-100 dark:bg-brand-900 text-brand-600 dark:text-brand-400 flex items-center justify-center text-sm font-medium flex-shrink-0">
{item.step}
</div>
<div className="flex-1">
<h4 className="font-medium text-gray-900 dark:text-white text-sm">
{item.title}
</h4>
<p className="text-xs text-gray-500 dark:text-gray-400">
{item.description}
</p>
</div>
{item.action && (
<div className="flex-shrink-0">
{item.action}
</div>
{platform.comingSoon && (
<span className="absolute top-2 right-2 text-xs px-2 py-0.5 rounded-full bg-warning-100 dark:bg-warning-900/50 text-warning-700 dark:text-warning-400">
Coming Soon
</span>
)}
</div>
<span className="text-3xl mb-3 block">{platform.icon}</span>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-1">
{platform.name}
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">
{platform.description}
</p>
</button>
))}
</div>
{/* Test Connection */}
<Card className="p-4 mb-6 border-2 border-dashed 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-lg flex items-center justify-center ${
testResult === 'success'
? 'bg-success-100 dark:bg-success-900/50 text-success-600 dark:text-success-400'
: testResult === 'failed'
? 'bg-error-100 dark:bg-error-900/50 text-error-600 dark:text-error-400'
: 'bg-gray-100 dark:bg-gray-800 text-gray-500'
}`}>
{testResult === 'success' ? (
<CheckCircleIcon className="w-5 h-5" />
) : (
<PlugInIcon className="w-5 h-5" />
)}
</div>
<div>
<h4 className="font-medium text-gray-900 dark:text-white text-sm">
Connection Status
</h4>
<p className="text-xs text-gray-500">
{testResult === 'success'
? 'Successfully connected to WordPress'
: testResult === 'failed'
? 'Connection failed - check plugin installation'
: 'Test the connection after installing the plugin'}
</p>
</div>
{/* WordPress Setup (shown when WordPress is selected) */}
{selectedPlatform === 'wordpress' && (
<>
{/* API Key Section */}
{apiKey && (
<Card className="p-5 mb-6 bg-gray-50 dark:bg-gray-800">
<label className="block text-base font-medium text-gray-700 dark:text-gray-300 mb-3">
Your API Key
</label>
<div className="flex items-center gap-3">
<code className="flex-1 px-4 py-3 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-lg text-base font-mono truncate">
{apiKey}
</code>
<Button
variant="outline"
size="md"
onClick={handleCopyApiKey}
className="flex-shrink-0"
startIcon={<CopyIcon className="w-4 h-4" />}
>
Copy
</Button>
</div>
</Card>
)}
{/* Installation Steps */}
<div className="space-y-3 mb-6">
{INSTALLATION_STEPS.map((item) => (
<div
key={item.step}
className="flex items-start gap-4 p-4 bg-gray-50 dark:bg-gray-800 rounded-xl"
>
<div className="size-8 rounded-full bg-brand-100 dark:bg-brand-900 text-brand-600 dark:text-brand-400 flex items-center justify-center text-base font-semibold flex-shrink-0">
{item.step}
</div>
<div className="flex-1">
<h4 className="font-semibold text-gray-900 dark:text-white text-base">
{item.title}
</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
{item.description}
</p>
</div>
{item.action && (
<div className="flex-shrink-0">
{item.action}
</div>
)}
</div>
))}
</div>
<Button
variant={testResult === 'success' ? 'primary' : 'outline'}
size="sm"
onClick={handleTestConnection}
disabled={isTesting}
startIcon={
isTesting ? <TimeIcon className="w-4 h-4 animate-spin" /> :
testResult === 'success' ? <CheckCircleIcon className="w-4 h-4" /> :
<TimeIcon className="w-4 h-4" />
}
>
{isTesting ? 'Testing...' : testResult === 'success' ? 'Connected' : 'Test Connection'}
</Button>
</div>
</Card>
{/* Test Connection */}
<Card className="p-5 mb-6 border-2 border-dashed border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className={`size-12 rounded-xl flex items-center justify-center ${
testResult === 'success'
? 'bg-success-100 dark:bg-success-900/50 text-success-600 dark:text-success-400'
: testResult === 'failed'
? 'bg-error-100 dark:bg-error-900/50 text-error-600 dark:text-error-400'
: 'bg-gray-100 dark:bg-gray-800 text-gray-500'
}`}>
{testResult === 'success' ? (
<CheckCircleIcon className="w-6 h-6" />
) : (
<PlugInIcon className="w-6 h-6" />
)}
</div>
<div>
<h4 className="font-semibold text-gray-900 dark:text-white text-base">
Connection Status
</h4>
<p className="text-sm text-gray-500">
{testResult === 'success'
? 'Successfully connected to WordPress'
: testResult === 'failed'
? 'Connection failed - check plugin installation'
: 'Test the connection after installing the plugin'}
</p>
</div>
</div>
<Button
variant={testResult === 'success' ? 'primary' : 'outline'}
size="md"
onClick={handleTestConnection}
disabled={isTesting}
startIcon={
isTesting ? <TimeIcon className="w-4 h-4 animate-spin" /> :
testResult === 'success' ? <CheckCircleIcon className="w-4 h-4" /> :
<TimeIcon className="w-4 h-4" />
}
>
{isTesting ? 'Testing...' : testResult === 'success' ? 'Connected' : 'Test Connection'}
</Button>
</div>
</Card>
</>
)}
{/* Info Alert */}
<Alert
variant="info"
title="Skip for now"
message="You can skip this step and set up the WordPress integration later from Site Settings → Integrations."
message="You can skip this step and set up the integration later from Site Settings → Integrations."
/>
{/* Actions */}
<div className="flex items-center justify-between">
<div className="flex items-center justify-between mt-8 pt-6 border-t border-gray-200 dark:border-gray-700">
<Button
variant="ghost"
tone="neutral"
size="md"
onClick={onBack}
startIcon={<ArrowLeftIcon className="w-4 h-4" />}
startIcon={<ArrowLeftIcon className="w-5 h-5" />}
>
Back
</Button>
<div className="flex gap-2">
<span className="text-sm text-gray-500 dark:text-gray-400">
Step <span className="font-semibold text-gray-700 dark:text-gray-300">{currentStep}</span> of <span className="font-semibold text-gray-700 dark:text-gray-300">{totalSteps}</span>
</span>
<div className="flex gap-3">
<Button
variant="ghost"
tone="neutral"
size="md"
onClick={onSkip}
>
Skip for now
</Button>
<Button
variant="primary"
size="md"
onClick={onNext}
endIcon={<ArrowRightIcon className="w-4 h-4" />}
endIcon={<ArrowRightIcon className="w-5 h-5" />}
>
Continue
</Button>

View File

@@ -1,11 +1,11 @@
/**
* Step 4: Add Keywords
* Initial keyword input for content pipeline
* Supports two modes: High Opportunity Keywords or Manual Entry
*/
import React, { useState } from 'react';
import React, { useState, useEffect, useMemo } from 'react';
import Button from '../../ui/button/Button';
import IconButton from '../../ui/button/IconButton';
import InputField from '../../form/input/InputField';
import { Card } from '../../ui/card';
import Badge from '../../ui/badge/Badge';
import Alert from '../../ui/alert/Alert';
@@ -15,8 +15,18 @@ import {
ListIcon,
PlusIcon,
CloseIcon,
BoltIcon,
PencilIcon,
CheckCircleIcon,
} from '../../../icons';
import { createKeyword } from '../../../services/api';
import {
createKeyword,
fetchSeedKeywords,
addSeedKeywordsToWorkflow,
fetchSiteSectors,
fetchIndustries,
SeedKeyword,
} from '../../../services/api';
import { useToast } from '../../ui/toast/ToastContainer';
import type { WizardData } from '../OnboardingWizard';
@@ -26,6 +36,24 @@ interface Step4AddKeywordsProps {
onNext: () => void;
onBack: () => void;
onSkip: () => void;
currentStep: number;
totalSteps: number;
}
type KeywordMode = 'opportunity' | 'manual' | null;
interface SectorKeywordOption {
type: 'high-volume' | 'low-difficulty';
label: string;
keywords: SeedKeyword[];
added: boolean;
}
interface SectorKeywordData {
sectorSlug: string;
sectorName: string;
sectorId: number;
options: SectorKeywordOption[];
}
export default function Step4AddKeywords({
@@ -33,14 +61,182 @@ export default function Step4AddKeywords({
updateData,
onNext,
onBack,
onSkip
onSkip,
currentStep,
totalSteps,
}: Step4AddKeywordsProps) {
const toast = useToast();
const [mode, setMode] = useState<KeywordMode>(null);
const [keywords, setKeywords] = useState<string[]>([]);
const [inputValue, setInputValue] = useState('');
const [isAdding, setIsAdding] = useState(false);
const [error, setError] = useState<string | null>(null);
// High opportunity keywords state - by sector
const [loadingOpportunities, setLoadingOpportunities] = useState(false);
const [sectorKeywordData, setSectorKeywordData] = useState<SectorKeywordData[]>([]);
// Load sector keywords when opportunity mode is selected
useEffect(() => {
if (mode === 'opportunity' && data.selectedSectors.length > 0 && data.createdSiteId) {
loadSectorKeywords();
}
}, [mode, data.selectedSectors, data.createdSiteId]);
const loadSectorKeywords = async () => {
if (!data.createdSiteId || !data.industrySlug) {
setError('Site must be created first to load keywords');
return;
}
setLoadingOpportunities(true);
setError(null);
try {
// Get site sectors with their IDs
const siteSectors = await fetchSiteSectors(data.createdSiteId);
// Get industry ID from industries API
const industriesResponse = await fetchIndustries();
const industry = industriesResponse.industries?.find(i => i.slug === data.industrySlug);
if (!industry?.id) {
setError('Could not find industry information');
return;
}
const sectorData: SectorKeywordData[] = [];
for (const sectorSlug of data.selectedSectors) {
// Find the site sector to get its ID
const siteSector = siteSectors.find((s: any) => s.slug === sectorSlug);
if (!siteSector) continue;
// Find the industry sector ID for this sector slug
const industrySector = industry.sectors?.find(s => s.slug === sectorSlug);
if (!industrySector) continue;
// Get the sector ID from industry_sector relationship
const sectorId = siteSector.id;
const sectorName = siteSector.name || sectorSlug.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
// Fetch seed keywords for this sector from the database
// Get all keywords sorted by volume (descending) for high volume
const highVolumeResponse = await fetchSeedKeywords({
industry: industry.id,
page_size: 500, // Get enough to filter
});
// Filter keywords matching this sector slug and sort by volume
const sectorKeywords = highVolumeResponse.results.filter(
kw => kw.sector_slug === sectorSlug
);
// Top 50 by highest volume
const highVolumeKeywords = [...sectorKeywords]
.sort((a, b) => (b.volume || 0) - (a.volume || 0))
.slice(0, 50);
// Top 50 by lowest difficulty (KD)
const lowDifficultyKeywords = [...sectorKeywords]
.sort((a, b) => (a.difficulty || 100) - (b.difficulty || 100))
.slice(0, 50);
sectorData.push({
sectorSlug,
sectorName,
sectorId,
options: [
{
type: 'high-volume',
label: 'Top 50 High Volume',
keywords: highVolumeKeywords,
added: false,
},
{
type: 'low-difficulty',
label: 'Top 50 Low Difficulty',
keywords: lowDifficultyKeywords,
added: false,
},
],
});
}
setSectorKeywordData(sectorData);
if (sectorData.length === 0) {
setError('No seed keywords found for your selected sectors. Try adding keywords manually.');
}
} catch (err: any) {
console.error('Failed to load sector keywords:', err);
setError(err.message || 'Failed to load keywords from database');
} finally {
setLoadingOpportunities(false);
}
};
// Track which option is being added (sectorSlug-optionType)
const [addingOption, setAddingOption] = useState<string | null>(null);
// Handle adding all keywords from a sector option using bulk API
const handleAddSectorKeywords = async (sectorSlug: string, optionType: 'high-volume' | 'low-difficulty') => {
const sector = sectorKeywordData.find(s => s.sectorSlug === sectorSlug);
if (!sector || !data.createdSiteId) {
setError('Site must be created before adding keywords');
return;
}
const option = sector.options.find(o => o.type === optionType);
if (!option || option.added || option.keywords.length === 0) return;
const addingKey = `${sectorSlug}-${optionType}`;
setAddingOption(addingKey);
setError(null);
try {
// Get seed keyword IDs
const seedKeywordIds = option.keywords.map(kw => kw.id);
// Use the bulk add API to add keywords to workflow
const result = await addSeedKeywordsToWorkflow(
seedKeywordIds,
data.createdSiteId,
sector.sectorId
);
if (result.success && result.created > 0) {
// Mark option as added
setSectorKeywordData(prev => prev.map(s =>
s.sectorSlug === sectorSlug
? {
...s,
options: s.options.map(o =>
o.type === optionType ? { ...o, added: true } : o
)
}
: s
));
// Update count
setKeywords(prev => [...prev, ...option.keywords.map(k => k.keyword)]);
let message = `Added ${result.created} keywords to ${data.siteName} - ${sector.sectorName}`;
if (result.skipped && result.skipped > 0) {
message += ` (${result.skipped} skipped)`;
}
toast.success(message);
} else if (result.errors && result.errors.length > 0) {
setError(result.errors[0]);
} else {
setError('No keywords were added. They may already exist in your workflow.');
}
} catch (err: any) {
setError(err.message || 'Failed to add keywords to workflow');
} finally {
setAddingOption(null);
}
};
const handleAddKeyword = () => {
const keyword = inputValue.trim();
@@ -126,66 +322,334 @@ export default function Step4AddKeywords({
}
};
const SUGGESTIONS = [
'best [product] for [use case]',
'how to [action] with [tool]',
'[topic] guide for beginners',
'[industry] trends 2025',
'[problem] solutions for [audience]',
];
// Mode selection view
if (mode === null) {
return (
<div>
<div className="mb-8">
<div className="flex items-center gap-3 mb-3">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
Add Target Keywords
</h2>
<Badge tone="neutral" variant="soft" size="sm">Optional</Badge>
</div>
<p className="text-base text-gray-600 dark:text-gray-400">
Add keywords to start your content pipeline. Choose how you'd like to add keywords.
</p>
</div>
{/* Mode Selection Cards - Icon and title in same row */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-8">
{/* High Opportunity Keywords */}
<button
onClick={() => setMode('opportunity')}
className="p-6 rounded-xl border-2 border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-left hover:border-brand-300 dark:hover:border-brand-700 hover:shadow-md transition-all group"
>
<div className="flex items-center gap-4 mb-3">
<div className="size-12 rounded-xl bg-brand-100 dark:bg-brand-900/50 flex items-center justify-center text-brand-600 dark:text-brand-400 group-hover:scale-110 transition-transform flex-shrink-0">
<BoltIcon className="w-6 h-6" />
</div>
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">
High Opportunity Keywords
</h3>
</div>
<p className="text-base text-gray-500 dark:text-gray-400 mb-3">
Select top keywords curated for your sectors. Includes volume and difficulty rankings.
</p>
<div className="flex items-center gap-2 text-brand-600 dark:text-brand-400 font-medium">
<span>Select top keywords</span>
<ArrowRightIcon className="w-4 h-4" />
</div>
</button>
{/* Manual Entry */}
<button
onClick={() => setMode('manual')}
className="p-6 rounded-xl border-2 border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-left hover:border-brand-300 dark:hover:border-brand-700 hover:shadow-md transition-all group"
>
<div className="flex items-center gap-4 mb-3">
<div className="size-12 rounded-xl bg-gray-100 dark:bg-gray-700 flex items-center justify-center text-gray-600 dark:text-gray-400 group-hover:scale-110 transition-transform flex-shrink-0">
<PencilIcon className="w-6 h-6" />
</div>
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">
Add Manually
</h3>
</div>
<p className="text-base text-gray-500 dark:text-gray-400 mb-3">
Enter your own keywords one by one or paste a list. Perfect if you have specific topics in mind.
</p>
<div className="flex items-center gap-2 text-gray-600 dark:text-gray-400 font-medium">
<span>Type or paste keywords</span>
<ArrowRightIcon className="w-4 h-4" />
</div>
</button>
</div>
{/* Selected Sectors Display */}
{data.selectedSectors.length > 0 && (
<Card className="p-5 bg-gray-50 dark:bg-gray-800/50 mb-6">
<h4 className="text-base font-medium text-gray-700 dark:text-gray-300 mb-3">
Your Selected Sectors
</h4>
<div className="flex flex-wrap gap-2">
{data.selectedSectors.map((sector) => (
<Badge key={sector} tone="brand" variant="soft" size="md" className="text-base px-3 py-1.5">
{sector.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}
</Badge>
))}
</div>
</Card>
)}
{/* Actions */}
<div className="flex items-center justify-between pt-4 border-t border-gray-200 dark:border-gray-700">
<Button
variant="ghost"
tone="neutral"
size="md"
onClick={onBack}
startIcon={<ArrowLeftIcon className="w-5 h-5" />}
>
Back
</Button>
<span className="text-sm text-gray-500 dark:text-gray-400">
Step <span className="font-semibold text-gray-700 dark:text-gray-300">{currentStep}</span> of <span className="font-semibold text-gray-700 dark:text-gray-300">{totalSteps}</span>
</span>
<Button
variant="ghost"
tone="neutral"
size="md"
onClick={onSkip}
>
Skip for now
</Button>
</div>
</div>
);
}
// High Opportunity Keywords mode
if (mode === 'opportunity') {
const addedCount = sectorKeywordData.reduce((acc, s) =>
acc + s.options.filter(o => o.added).reduce((sum, o) => sum + o.keywords.length, 0), 0
);
const allOptionsAdded = sectorKeywordData.every(s => s.options.every(o => o.added));
return (
<div>
<div className="mb-8">
<button
onClick={() => setMode(null)}
className="flex items-center gap-2 text-base text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 mb-4"
>
<ArrowLeftIcon className="w-4 h-4" />
<span>Back to options</span>
</button>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
High Opportunity Keywords
</h2>
<p className="text-base text-gray-600 dark:text-gray-400">
Add top keywords for each of your sectors. Keywords will be added to your planner workflow.
</p>
</div>
{error && (
<Alert variant="error" title="Error" message={error} className="mb-6" />
)}
{loadingOpportunities ? (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-brand-500" />
</div>
) : (
<>
{/* Sector columns with 2 options each */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-6 items-start">
{sectorKeywordData.map((sector) => (
<div key={sector.sectorSlug} className="flex flex-col gap-3">
<h4 className="text-lg font-semibold text-gray-900 dark:text-white border-b border-gray-200 dark:border-gray-700 pb-2">
{sector.sectorName}
</h4>
{sector.options.map((option) => {
const addingKey = `${sector.sectorSlug}-${option.type}`;
const isAdding = addingOption === addingKey;
return (
<Card
key={option.type}
className={`p-4 transition-all flex-1 flex flex-col ${
option.added
? 'border-success-300 dark:border-success-700 bg-success-50 dark:bg-success-900/20'
: 'hover:border-brand-300 dark:hover:border-brand-700'
}`}
>
<div className="flex items-center justify-between mb-3">
<div>
<h5 className="text-base font-medium text-gray-900 dark:text-white">
{option.label}
</h5>
<p className="text-xs text-gray-500 dark:text-gray-400">
{option.keywords.length} keywords
</p>
</div>
{option.added ? (
<Badge tone="success" variant="soft" size="sm">
<CheckCircleIcon className="w-3 h-3 mr-1" />
Added
</Badge>
) : (
<Button
variant="primary"
size="xs"
onClick={() => handleAddSectorKeywords(sector.sectorSlug, option.type)}
disabled={isAdding}
>
{isAdding ? 'Adding...' : 'Add All'}
</Button>
)}
</div>
{/* Show first 3 keywords with +X more */}
<div className="flex flex-wrap gap-1.5 flex-1">
{option.keywords.slice(0, 3).map((kw) => (
<Badge
key={kw.id}
tone={option.added ? 'success' : 'neutral'}
variant="soft"
size="xs"
className="text-xs"
>
{kw.keyword}
</Badge>
))}
{option.keywords.length > 3 && (
<Badge tone="neutral" variant="outline" size="xs" className="text-xs">
+{option.keywords.length - 3} more
</Badge>
)}
</div>
</Card>
);
})}
</div>
))}
</div>
{/* Summary */}
{addedCount > 0 && (
<Card className="p-4 bg-success-50 dark:bg-success-900/20 border-success-200 dark:border-success-800 mb-6">
<div className="flex items-center gap-3">
<CheckCircleIcon className="w-5 h-5 text-success-600 dark:text-success-400" />
<span className="text-base text-success-700 dark:text-success-300">
{addedCount} keywords added to your workflow
</span>
</div>
</Card>
)}
</>
)}
{/* Actions */}
<div className="flex items-center justify-between pt-4 border-t border-gray-200 dark:border-gray-700">
<Button
variant="ghost"
tone="neutral"
size="md"
onClick={() => setMode(null)}
startIcon={<ArrowLeftIcon className="w-5 h-5" />}
>
Back
</Button>
<span className="text-sm text-gray-500 dark:text-gray-400">
Step <span className="font-semibold text-gray-700 dark:text-gray-300">{currentStep}</span> of <span className="font-semibold text-gray-700 dark:text-gray-300">{totalSteps}</span>
</span>
<div className="flex gap-3">
{!allOptionsAdded && (
<Button
variant="ghost"
tone="neutral"
size="md"
onClick={onSkip}
>
Skip for now
</Button>
)}
<Button
variant="primary"
size="md"
onClick={onNext}
disabled={addedCount === 0}
endIcon={<ArrowRightIcon className="w-5 h-5" />}
>
{addedCount > 0 ? 'Continue' : 'Add keywords first'}
</Button>
</div>
</div>
</div>
);
}
// Manual entry mode
return (
<div>
<div className="mb-6">
<div className="flex items-center gap-2 mb-2">
<h2 className="text-xl font-bold text-gray-900 dark:text-white">
Add Target Keywords
</h2>
<Badge tone="neutral" variant="soft">Optional</Badge>
</div>
<p className="text-gray-600 dark:text-gray-400">
Add keywords to start your content pipeline. These will be used to generate content ideas and articles.
<div className="mb-8">
<button
onClick={() => setMode(null)}
className="flex items-center gap-2 text-base text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 mb-4"
>
<ArrowLeftIcon className="w-4 h-4" />
<span>Back to options</span>
</button>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
Add Keywords Manually
</h2>
<p className="text-base text-gray-600 dark:text-gray-400">
Enter keywords one by one or paste a list. Press Enter or click + to add each keyword.
</p>
</div>
{error && (
<Alert variant="error" title="Error" message={error} />
<Alert variant="error" title="Error" message={error} className="mb-6" />
)}
{/* Keyword Input */}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
<div className="mb-6">
<label className="block text-base font-medium text-gray-700 dark:text-gray-300 mb-2">
Enter keywords (press Enter or paste multiple)
</label>
<div className="flex gap-2">
<div className="flex gap-3">
<div className="relative flex-1">
<ListIcon className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<InputField
<ListIcon className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
onPaste={handlePaste}
placeholder="Enter a keyword..."
className="pl-10"
className="w-full pl-12 pr-4 py-3 text-base border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:ring-2 focus:ring-brand-500 focus:border-transparent"
/>
</div>
<IconButton
variant="outline"
size="md"
onClick={handleAddKeyword}
disabled={!inputValue.trim()}
icon={<PlusIcon className="w-4 h-4" />}
icon={<PlusIcon className="w-5 h-5" />}
/>
</div>
<p className="text-xs text-gray-500 mt-1">
<p className="text-sm text-gray-500 mt-2">
Tip: Paste a comma-separated list or one keyword per line
</p>
</div>
{/* Keywords List */}
<Card className="p-4 mb-4 min-h-[120px] bg-gray-50 dark:bg-gray-800">
<Card className="p-5 mb-6 min-h-[140px] bg-gray-50 dark:bg-gray-800">
{keywords.length === 0 ? (
<div className="text-center py-6 text-gray-500 dark:text-gray-400">
<ListIcon className="w-8 h-8 mx-auto mb-2 opacity-50" />
<p className="text-sm">No keywords added yet</p>
<p className="text-xs">Start typing or paste keywords above</p>
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
<ListIcon className="w-10 h-10 mx-auto mb-3 opacity-50" />
<p className="text-base">No keywords added yet</p>
<p className="text-sm">Start typing or paste keywords above</p>
</div>
) : (
<div className="flex flex-wrap gap-2">
@@ -194,11 +658,12 @@ export default function Step4AddKeywords({
key={keyword}
tone="neutral"
variant="soft"
className="gap-1 pr-1"
size="md"
className="gap-2 pr-2 text-base"
>
{keyword}
<IconButton
icon={<CloseIcon className="w-3 h-3" />}
icon={<CloseIcon className="w-4 h-4" />}
onClick={() => handleRemoveKeyword(keyword)}
variant="ghost"
size="xs"
@@ -211,7 +676,7 @@ export default function Step4AddKeywords({
)}
</Card>
<div className="flex items-center justify-between text-sm text-gray-500 mb-4">
<div className="flex items-center justify-between text-base text-gray-500 mb-6">
<span>{keywords.length} keyword{keywords.length !== 1 ? 's' : ''} added</span>
{keywords.length > 0 && (
<Button
@@ -225,53 +690,43 @@ export default function Step4AddKeywords({
)}
</div>
{/* Keyword Suggestions */}
<Card className="p-4 bg-brand-50 dark:bg-brand-900/20 border-brand-200 dark:border-brand-800 mb-6">
<h4 className="font-medium text-brand-900 dark:text-brand-100 text-sm mb-2">
Keyword Ideas
</h4>
<div className="flex flex-wrap gap-2">
{SUGGESTIONS.map((suggestion, index) => (
<span
key={index}
className="text-xs text-brand-700 dark:text-brand-300 bg-brand-100 dark:bg-brand-800/50 px-2 py-1 rounded"
>
{suggestion}
</span>
))}
</div>
</Card>
{/* Info Alert */}
<Alert
variant="info"
title="Add keywords later"
message="You can add more keywords later from the Planner page. The automation will process these keywords and generate content automatically."
title="Add more keywords later"
message="You can add more keywords from the Planner page. The automation will process these keywords and generate content automatically."
className="mb-6"
/>
{/* Actions */}
<div className="flex items-center justify-between">
<div className="flex items-center justify-between pt-4 border-t border-gray-200 dark:border-gray-700">
<Button
variant="ghost"
tone="neutral"
onClick={onBack}
startIcon={<ArrowLeftIcon className="w-4 h-4" />}
size="md"
onClick={() => setMode(null)}
startIcon={<ArrowLeftIcon className="w-5 h-5" />}
>
Back
</Button>
<div className="flex gap-2">
<span className="text-sm text-gray-500 dark:text-gray-400">
Step <span className="font-semibold text-gray-700 dark:text-gray-300">{currentStep}</span> of <span className="font-semibold text-gray-700 dark:text-gray-300">{totalSteps}</span>
</span>
<div className="flex gap-3">
<Button
variant="ghost"
tone="neutral"
size="md"
onClick={onSkip}
>
Skip for now
</Button>
<Button
variant="primary"
size="md"
onClick={handleSubmitKeywords}
disabled={isAdding || keywords.length === 0}
endIcon={!isAdding ? <ArrowRightIcon className="w-4 h-4" /> : undefined}
endIcon={!isAdding ? <ArrowRightIcon className="w-5 h-5" /> : undefined}
>
{isAdding ? 'Adding...' : `Add ${keywords.length} Keyword${keywords.length !== 1 ? 's' : ''}`}
</Button>

View File

@@ -1,167 +1,244 @@
/**
* Step 5: Complete
* Success screen with next steps
* Success screen with next steps - matches wizard styling
*/
import React from 'react';
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import Button from '../../ui/button/Button';
import { Card } from '../../ui/card';
import Badge from '../../ui/badge/Badge';
import {
CheckCircleIcon,
ArrowRightIcon,
BoltIcon,
FileTextIcon,
PieChartIcon,
BoxCubeIcon,
SettingsIcon,
ListIcon,
} from '../../../icons';
import { fetchKeywords } from '../../../services/api';
import type { WizardData } from '../OnboardingWizard';
interface Step5CompleteProps {
data: WizardData;
onComplete: () => void;
isLoading: boolean;
currentStep: number;
totalSteps: number;
}
export default function Step5Complete({
data,
onComplete,
isLoading
isLoading,
currentStep,
totalSteps,
}: Step5CompleteProps) {
const NEXT_STEPS = [
{
icon: <BoltIcon className="w-5 h-5" />,
title: 'Run Automation',
description: 'Start your content pipeline to generate articles',
link: data.createdSiteId ? `/sites/${data.createdSiteId}/automation` : '/automation',
},
{
icon: <FileTextIcon className="w-5 h-5" />,
title: 'Add More Keywords',
description: 'Expand your content strategy with more target keywords',
link: '/planner/keywords',
},
{
icon: <PieChartIcon className="w-5 h-5" />,
title: 'View Dashboard',
description: 'Monitor your content pipeline and metrics',
link: '/dashboard',
},
{
icon: <BoxCubeIcon className="w-5 h-5" />,
title: 'Customize Settings',
description: 'Fine-tune publishing schedules and preferences',
link: data.createdSiteId ? `/sites/${data.createdSiteId}/settings` : '/account/settings',
},
];
const navigate = useNavigate();
const [keywordCount, setKeywordCount] = useState<number>(0);
const [loadingCount, setLoadingCount] = useState(true);
// Fetch actual keyword count from planner for this site
useEffect(() => {
const loadKeywordCount = async () => {
if (!data.createdSiteId) {
setLoadingCount(false);
return;
}
try {
const response = await fetchKeywords({
site_id: data.createdSiteId,
page_size: 1, // We only need the count, not the data
});
setKeywordCount(response.count || 0);
} catch (err) {
console.error('Failed to fetch keyword count:', err);
setKeywordCount(data.keywordsCount || 0); // Fallback to wizard data
} finally {
setLoadingCount(false);
}
};
loadKeywordCount();
}, [data.createdSiteId, data.keywordsCount]);
const handleNavigate = (path: string) => {
navigate(path);
};
return (
<div className="text-center">
{/* Success Animation */}
<div className="mb-6">
<div className="inline-flex items-center justify-center size-20 rounded-full bg-success-100 dark:bg-success-900/50 text-success-600 dark:text-success-400 mb-4">
<CheckCircleIcon className="h-10 w-10" />
<div>
{/* Header - matches other steps */}
<div className="mb-8">
<div className="flex items-center gap-4">
<div className="size-16 rounded-xl bg-success-100 dark:bg-success-900/50 text-success-600 dark:text-success-400 flex items-center justify-center flex-shrink-0">
<CheckCircleIcon className="h-8 w-8" />
</div>
<div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
Setup Complete
</h2>
<p className="text-base text-gray-600 dark:text-gray-400">
Your content pipeline is configured and ready to generate content.
</p>
</div>
</div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
You're All Set! 🎉
</h1>
<p className="text-gray-600 dark:text-gray-400 max-w-md mx-auto">
Your content pipeline is ready to go. IGNY8 will start processing your keywords and generating content automatically.
</p>
</div>
{/* Summary */}
<Card className="p-4 mb-6 bg-gray-50 dark:bg-gray-800 text-left">
<h3 className="font-semibold text-gray-900 dark:text-white mb-3">
What we set up:
</h3>
<ul className="space-y-2">
<li className="flex items-center gap-2 text-sm">
<CheckCircleIcon className="w-4 h-4 text-success-500" />
<span className="text-gray-700 dark:text-gray-300">
Site: <span className="font-medium">{data.siteName || 'Your Site'}</span>
</span>
</li>
{data.integrationTested && (
<li className="flex items-center gap-2 text-sm">
<CheckCircleIcon className="w-4 h-4 text-success-500" />
<span className="text-gray-700 dark:text-gray-300">
WordPress integration connected
</span>
</li>
)}
{data.keywordsAdded && (
<li className="flex items-center gap-2 text-sm">
<CheckCircleIcon className="w-4 h-4 text-success-500" />
<span className="text-gray-700 dark:text-gray-300">
{data.keywordsCount} keyword{data.keywordsCount !== 1 ? 's' : ''} added to pipeline
</span>
</li>
)}
<li className="flex items-center gap-2 text-sm">
<CheckCircleIcon className="w-4 h-4 text-success-500" />
<span className="text-gray-700 dark:text-gray-300">
Auto-approval & auto-publish enabled
</span>
</li>
<li className="flex items-center gap-2 text-sm">
<CheckCircleIcon className="w-4 h-4 text-success-500" />
<span className="text-gray-700 dark:text-gray-300">
Daily automation scheduled
</span>
</li>
</ul>
</Card>
{/* Expected Timeline */}
<Card className="p-4 mb-6 bg-brand-50 dark:bg-brand-900/20 border-brand-200 dark:border-brand-800 text-left">
<h3 className="font-semibold text-brand-900 dark:text-brand-100 mb-2">
📅 What to expect:
</h3>
<ul className="text-sm text-brand-700 dark:text-brand-300 space-y-1">
<li>• First content ideas: Within 24 hours</li>
<li>• First articles ready: 2-3 days</li>
<li>• First published to site: Based on your schedule</li>
</ul>
<p className="text-xs text-brand-600 dark:text-brand-400 mt-2">
Run automation manually anytime to speed things up.
</p>
</Card>
{/* Next Steps */}
<div className="mb-6">
<h3 className="font-semibold text-gray-900 dark:text-white mb-3 text-left">
What's next:
</h3>
<div className="grid grid-cols-2 gap-3">
{NEXT_STEPS.map((step, index) => (
<div
key={index}
className="p-3 bg-gray-50 dark:bg-gray-800 rounded-lg text-left hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors cursor-pointer"
>
<div className="size-8 rounded-lg bg-brand-100 dark:bg-brand-900/50 text-brand-600 dark:text-brand-400 flex items-center justify-center mb-2">
{step.icon}
{/* 2-Column Layout: Summary + Next Steps */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
{/* Left Column - Configuration Summary */}
<Card className="p-5 bg-gray-50 dark:bg-gray-800/50 border border-gray-100 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Configuration Summary
</h3>
<div className="space-y-3">
<div className="flex items-center justify-between py-2 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-3">
<CheckCircleIcon className="w-5 h-5 text-success-500 flex-shrink-0" />
<span className="text-base text-gray-700 dark:text-gray-300">Site Added</span>
</div>
<h4 className="font-medium text-gray-900 dark:text-white text-sm">
{step.title}
</h4>
<p className="text-xs text-gray-500 dark:text-gray-400">
{step.description}
</p>
<span className="text-base font-medium text-gray-900 dark:text-white truncate max-w-[150px]">
{data.siteName || 'Your Site'}
</span>
</div>
))}
<div className="flex items-center justify-between py-2 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-3">
<CheckCircleIcon className="w-5 h-5 text-success-500 flex-shrink-0" />
<span className="text-base text-gray-700 dark:text-gray-300">Sectors</span>
</div>
<span className="text-base font-medium text-gray-900 dark:text-white">
{data.selectedSectors?.length || 0} selected
</span>
</div>
<div className="flex items-center justify-between py-2 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-3">
{data.integrationTested ? (
<CheckCircleIcon className="w-5 h-5 text-success-500 flex-shrink-0" />
) : (
<div className="w-5 h-5 rounded-full border-2 border-gray-300 dark:border-gray-600 flex-shrink-0" />
)}
<span className="text-base text-gray-700 dark:text-gray-300">WordPress</span>
</div>
<Badge
tone={data.integrationTested ? 'success' : 'neutral'}
variant="soft"
size="md"
>
{data.integrationTested ? 'Connected' : 'Skipped'}
</Badge>
</div>
<div className="flex items-center justify-between py-2">
<div className="flex items-center gap-3">
{keywordCount > 0 ? (
<CheckCircleIcon className="w-5 h-5 text-success-500 flex-shrink-0" />
) : (
<div className="w-5 h-5 rounded-full border-2 border-gray-300 dark:border-gray-600 flex-shrink-0" />
)}
<span className="text-base text-gray-700 dark:text-gray-300">Keywords</span>
</div>
<Badge
tone={keywordCount > 0 ? 'brand' : 'neutral'}
variant="soft"
size="md"
>
{loadingCount ? '...' : `${keywordCount} added`}
</Badge>
</div>
</div>
</Card>
{/* Right Column - Next Steps */}
<div className="space-y-3">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Next Steps
</h3>
{/* Run Automation */}
<Card
className="p-4 hover:border-brand-300 dark:hover:border-brand-700 transition-all cursor-pointer"
onClick={() => handleNavigate(data.createdSiteId ? `/sites/${data.createdSiteId}/automation` : '/automation')}
>
<div className="flex items-center gap-3">
<div className="size-10 rounded-lg bg-warning-100 dark:bg-warning-900/50 text-warning-600 dark:text-warning-400 flex items-center justify-center flex-shrink-0">
<BoltIcon className="w-5 h-5" />
</div>
<div className="flex-1 min-w-0">
<h4 className="text-base font-semibold text-gray-900 dark:text-white">
Run Automation
</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Generate content ideas and articles now
</p>
</div>
<ArrowRightIcon className="w-5 h-5 text-gray-400 flex-shrink-0" />
</div>
</Card>
{/* Manage Keywords */}
<Card
className="p-4 hover:border-brand-300 dark:hover:border-brand-700 transition-all cursor-pointer"
onClick={() => handleNavigate('/planner/keywords')}
>
<div className="flex items-center gap-3">
<div className="size-10 rounded-lg bg-brand-100 dark:bg-brand-900/50 text-brand-600 dark:text-brand-400 flex items-center justify-center flex-shrink-0">
<ListIcon className="w-5 h-5" />
</div>
<div className="flex-1 min-w-0">
<h4 className="text-base font-semibold text-gray-900 dark:text-white">
Manage Keywords
</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Add more keywords to your pipeline
</p>
</div>
<Badge tone="brand" variant="soft" size="sm" className="flex-shrink-0">
{loadingCount ? '...' : keywordCount}
</Badge>
<ArrowRightIcon className="w-5 h-5 text-gray-400 flex-shrink-0" />
</div>
</Card>
{/* Site Settings */}
<Card
className="p-4 hover:border-brand-300 dark:hover:border-brand-700 transition-all cursor-pointer"
onClick={() => handleNavigate(data.createdSiteId ? `/sites/${data.createdSiteId}/settings` : '/account/settings')}
>
<div className="flex items-center gap-3">
<div className="size-10 rounded-lg bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 flex items-center justify-center flex-shrink-0">
<SettingsIcon className="w-5 h-5" />
</div>
<div className="flex-1 min-w-0">
<h4 className="text-base font-semibold text-gray-900 dark:text-white">
Site Settings
</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">
Configure publishing and automation
</p>
</div>
<ArrowRightIcon className="w-5 h-5 text-gray-400 flex-shrink-0" />
</div>
</Card>
</div>
</div>
{/* CTA */}
<Button
variant="primary"
size="lg"
onClick={onComplete}
disabled={isLoading}
fullWidth
endIcon={!isLoading ? <ArrowRightIcon className="w-4 h-4" /> : undefined}
>
{isLoading ? 'Loading...' : 'Go to Dashboard'}
</Button>
{/* Footer Actions */}
<div className="flex items-center justify-between pt-4 border-t border-gray-200 dark:border-gray-700">
<span className="text-sm text-gray-500 dark:text-gray-400">
Step <span className="font-semibold text-gray-700 dark:text-gray-300">{currentStep}</span> of <span className="font-semibold text-gray-700 dark:text-gray-300">{totalSteps}</span>
</span>
<Button
variant="primary"
size="lg"
onClick={onComplete}
disabled={isLoading}
endIcon={!isLoading ? <ArrowRightIcon className="w-5 h-5" /> : undefined}
>
{isLoading ? 'Loading...' : 'Go to Dashboard'}
</Button>
</div>
</div>
);
}

View File

@@ -30,6 +30,10 @@ import AIOperationsWidget, { AIOperationsData } from "../../components/dashboard
import RecentActivityWidget, { ActivityItem } from "../../components/dashboard/RecentActivityWidget";
import ContentVelocityWidget, { ContentVelocityData } from "../../components/dashboard/ContentVelocityWidget";
import AutomationStatusWidget, { AutomationData } from "../../components/dashboard/AutomationStatusWidget";
import SitesOverviewWidget from "../../components/dashboard/SitesOverviewWidget";
import CreditsUsageWidget from "../../components/dashboard/CreditsUsageWidget";
import AccountInfoWidget from "../../components/dashboard/AccountInfoWidget";
import { getSubscriptions, Subscription } from "../../services/billing.api";
export default function Home() {
const toast = useToast();
@@ -37,7 +41,7 @@ export default function Home() {
const { activeSector } = useSectorStore();
const { isGuideDismissed, showGuide, loadFromBackend } = useOnboardingStore();
const { user } = useAuthStore();
const { loadBalance } = useBillingStore();
const { balance, loadBalance } = useBillingStore();
const { setPageInfo } = usePageContext();
// Core state
@@ -46,6 +50,7 @@ export default function Home() {
const [siteFilter, setSiteFilter] = useState<'all' | number>('all');
const [showAddSite, setShowAddSite] = useState(false);
const [loading, setLoading] = useState(true);
const [subscription, setSubscription] = useState<Subscription | null>(null);
// Dashboard data state
const [attentionItems, setAttentionItems] = useState<AttentionItem[]>([]);
@@ -107,9 +112,22 @@ export default function Home() {
useEffect(() => {
loadSites();
loadBalance();
loadSubscription();
loadFromBackend().catch(() => {});
}, [loadFromBackend, loadBalance]);
// Load subscription info
const loadSubscription = async () => {
try {
const { results } = await getSubscriptions();
// Get the active subscription
const activeSubscription = results.find(s => s.status === 'active') || results[0] || null;
setSubscription(activeSubscription);
} catch (error) {
console.error('Failed to load subscription:', error);
}
};
// Load active site if not set
useEffect(() => {
if (!activeSite && sites.length > 0) {
@@ -343,13 +361,38 @@ export default function Home() {
onDismiss={handleDismissAttention}
/>
{/* Row 1: Workflow Pipeline (full width) */}
{/* Row 1: Sites Overview + Credits + Account (3 columns) */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-5">
<SitesOverviewWidget
sites={sites}
loading={sitesLoading}
onAddSite={canAddMoreSites ? handleAddSiteClick : undefined}
maxSites={maxSites}
/>
<CreditsUsageWidget
balance={balance}
aiOperations={{
total: aiOperations.totals.count,
period: aiOperations.period === '7d' ? 'Last 7 days' : aiOperations.period === '30d' ? 'Last 30 days' : 'Last 90 days',
}}
loading={loading}
/>
<AccountInfoWidget
balance={balance}
subscription={subscription}
plan={subscription?.plan && typeof subscription.plan === 'object' ? subscription.plan : null}
userPlan={(user?.account as any)?.plan}
loading={loading}
/>
</div>
{/* Row 2: Workflow Pipeline (full width) */}
<WorkflowPipelineWidget data={pipelineData} loading={loading} />
{/* Row 2: Workflow Guide (full width) */}
{/* Row 3: Quick Actions (full width) */}
<QuickActionsWidget />
{/* Row 3: AI Operations + Recent Activity */}
{/* Row 4: AI Operations + Recent Activity */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5">
<AIOperationsWidget
data={aiOperations}
@@ -359,7 +402,7 @@ export default function Home() {
<RecentActivityWidget activities={recentActivity} loading={loading} />
</div>
{/* Row 4: Content Velocity + Automation Status */}
{/* Row 5: Content Velocity + Automation Status */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5">
<ContentVelocityWidget data={contentVelocity} loading={loading} />
<AutomationStatusWidget

View File

@@ -1,15 +1,27 @@
/**
* Setup Wizard Page
* Wraps the OnboardingWizard component for direct access via sidebar
* Redesigned to proper page style with cleaner intro cards
* Can be accessed anytime, not just for new users
*/
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import OnboardingWizard from '../../components/onboarding/OnboardingWizard';
import PageHeader from '../../components/common/PageHeader';
import PageMeta from '../../components/common/PageMeta';
import { usePageContext } from '../../context/PageContext';
import { ShootingStarIcon } from '../../icons';
export default function SetupWizard() {
const navigate = useNavigate();
const { setPageInfo } = usePageContext();
// Set page info for AppHeader
useEffect(() => {
setPageInfo({
title: 'Setup Wizard',
badge: { icon: <ShootingStarIcon className="w-4 h-4" />, color: 'purple' },
});
return () => setPageInfo(null);
}, [setPageInfo]);
const handleComplete = () => {
navigate('/dashboard');
@@ -20,19 +32,15 @@ export default function SetupWizard() {
};
return (
<div className="space-y-6">
<PageHeader
title="Setup Wizard"
badge={{ icon: <ShootingStarIcon className="w-5 h-5" />, color: 'blue' }}
<>
<PageMeta
title="Setup Wizard - IGNY8"
description="Complete guided setup for your site"
/>
<div className="py-4">
<OnboardingWizard
onComplete={handleComplete}
onSkip={handleSkip}
/>
</div>
</div>
<OnboardingWizard
onComplete={handleComplete}
onSkip={handleSkip}
/>
</>
);
}