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

@@ -28,7 +28,7 @@
---
# PHASE 1: App UI Quick Fixes
## 1.1 - Credits Display Fix
## 1.1 - Credits Display Fix
**Location**: App header component
@@ -38,7 +38,7 @@
---
## 1.2 - Sites Card Redesign
## 1.2 - Sites Card Redesign
**Location**: Sites listing/grid component
@@ -50,7 +50,7 @@
---
## 1.3 - Page Loading Standardization
## 1.3 - Page Loading Standardization
**Current problem**: Each page has its own loading spinner/text implementation
@@ -64,7 +64,7 @@
---
## 1.4 - Global Layout Spacing
## 1.4 - Global Layout Spacing
**Required**:
- Define exact padding in global app layout ONLY:
@@ -79,7 +79,7 @@
---
## 1.5 - Button Standardization
## 1.5 - Button Standardization
**Current problems**:
1. Icon-on-top bug: Some buttons render icon above text instead of inline
@@ -111,7 +111,7 @@ grep -rn "<button" src/ --include="*.jsx" --include="*.tsx"
---
## 1.6 - Legacy Cleanup
## 1.6 - Legacy Cleanup
**Action**: Remove all unused pages, routes, and related data from:
- System codebase
@@ -123,7 +123,7 @@ grep -rn "<button" src/ --include="*.jsx" --include="*.tsx"
# PHASE 2: App Features & Improvements
## 2.1 - Setup Wizard Redesign
## 2.1 - Setup Wizard Redesign
**Current problems**:
- Converted from modal - not a good design
@@ -133,15 +133,14 @@ grep -rn "<button" src/ --include="*.jsx" --include="*.tsx"
**Required changes**:
1. Convert to proper page style (like other app pages)
2. Remove unnecessary header from wizard main page
3. Remove unnecessary icon from wizard main page
4. Create cleaner, better intro cards that clearly explain:
3. Create cleaner, better intro cards that clearly explain:
- What is being provided through wizard
- What each step accomplishes
- What user will have after completion
---
## 2.2 - Dashboard Widgets (3 new)
## 2.2 - Dashboard Widgets (3 new)
**Location**: Home/Dashboard page
@@ -171,7 +170,7 @@ Display the following account-related information:
---
## 2.3 - WordPress & Content Templates
## 2.3 - WordPress & Content Templates
### 2.3.1 - Post Template Optimization
- Review and optimize WordPress post template structure

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,15 +61,183 @@ 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();
if (!keyword) return;
@@ -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}
/>
</>
);
}