Phase 2, 2.1 and 2.2 complete
This commit is contained in:
@@ -28,7 +28,7 @@
|
|||||||
---
|
---
|
||||||
# PHASE 1: App UI Quick Fixes
|
# PHASE 1: App UI Quick Fixes
|
||||||
|
|
||||||
## 1.1 - Credits Display Fix
|
## 1.1 - Credits Display Fix ✅
|
||||||
|
|
||||||
**Location**: App header component
|
**Location**: App header component
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1.2 - Sites Card Redesign
|
## 1.2 - Sites Card Redesign ✅
|
||||||
|
|
||||||
**Location**: Sites listing/grid component
|
**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
|
**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**:
|
**Required**:
|
||||||
- Define exact padding in global app layout ONLY:
|
- Define exact padding in global app layout ONLY:
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1.5 - Button Standardization
|
## 1.5 - Button Standardization ✅
|
||||||
|
|
||||||
**Current problems**:
|
**Current problems**:
|
||||||
1. Icon-on-top bug: Some buttons render icon above text instead of inline
|
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:
|
**Action**: Remove all unused pages, routes, and related data from:
|
||||||
- System codebase
|
- System codebase
|
||||||
@@ -123,7 +123,7 @@ grep -rn "<button" src/ --include="*.jsx" --include="*.tsx"
|
|||||||
|
|
||||||
# PHASE 2: App Features & Improvements
|
# PHASE 2: App Features & Improvements
|
||||||
|
|
||||||
## 2.1 - Setup Wizard Redesign
|
## 2.1 - Setup Wizard Redesign ✅
|
||||||
|
|
||||||
**Current problems**:
|
**Current problems**:
|
||||||
- Converted from modal - not a good design
|
- Converted from modal - not a good design
|
||||||
@@ -133,15 +133,14 @@ grep -rn "<button" src/ --include="*.jsx" --include="*.tsx"
|
|||||||
**Required changes**:
|
**Required changes**:
|
||||||
1. Convert to proper page style (like other app pages)
|
1. Convert to proper page style (like other app pages)
|
||||||
2. Remove unnecessary header from wizard main page
|
2. Remove unnecessary header from wizard main page
|
||||||
3. Remove unnecessary icon from wizard main page
|
3. Create cleaner, better intro cards that clearly explain:
|
||||||
4. Create cleaner, better intro cards that clearly explain:
|
|
||||||
- What is being provided through wizard
|
- What is being provided through wizard
|
||||||
- What each step accomplishes
|
- What each step accomplishes
|
||||||
- What user will have after completion
|
- What user will have after completion
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2.2 - Dashboard Widgets (3 new)
|
## 2.2 - Dashboard Widgets (3 new) ✅
|
||||||
|
|
||||||
**Location**: Home/Dashboard page
|
**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
|
### 2.3.1 - Post Template Optimization
|
||||||
- Review and optimize WordPress post template structure
|
- Review and optimize WordPress post template structure
|
||||||
|
|||||||
@@ -21,11 +21,11 @@ const ComponentCard: React.FC<ComponentCardProps> = ({
|
|||||||
{(title || desc || headerContent) && (
|
{(title || desc || headerContent) && (
|
||||||
<div className="px-6 py-5 relative z-0 flex items-start justify-between gap-4">
|
<div className="px-6 py-5 relative z-0 flex items-start justify-between gap-4">
|
||||||
<div className="flex-1">
|
<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}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
{desc && (
|
{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}
|
{desc}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
208
frontend/src/components/dashboard/AccountInfoWidget.tsx
Normal file
208
frontend/src/components/dashboard/AccountInfoWidget.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
134
frontend/src/components/dashboard/CreditsUsageWidget.tsx
Normal file
134
frontend/src/components/dashboard/CreditsUsageWidget.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
171
frontend/src/components/dashboard/SitesOverviewWidget.tsx
Normal file
171
frontend/src/components/dashboard/SitesOverviewWidget.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -55,12 +55,12 @@ const stages = [
|
|||||||
{ key: 'published', label: 'Published', icon: PaperPlaneIcon, href: '/writer/published', gradient: 'from-success-500 to-success-600' },
|
{ key: 'published', label: 'Published', icon: PaperPlaneIcon, href: '/writer/published', gradient: 'from-success-500 to-success-600' },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
// Small filled arrow triangle component
|
// Filled arrow triangle component - positioned at end of flex item
|
||||||
function ArrowTip() {
|
function ArrowTip() {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center w-4 h-4 mx-1">
|
<div className="flex items-center justify-center w-6 h-6">
|
||||||
<svg viewBox="0 0 8 12" className="w-2.5 h-3.5 fill-brand-500 dark:fill-brand-400">
|
<svg viewBox="0 0 10 16" className="w-4 h-5 fill-brand-500 dark:fill-brand-400">
|
||||||
<path d="M0 0 L8 6 L0 12 Z" />
|
<path d="M0 0 L10 8 L0 16 Z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</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">
|
<div className="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-800 p-5">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between mb-5">
|
<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">
|
<div>
|
||||||
Workflow Pipeline
|
<h3 className="text-sm font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wide">
|
||||||
</h3>
|
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">
|
<span className="text-3xl font-bold text-brand-600 dark:text-brand-400">
|
||||||
{data.completionPercentage}%
|
{data.completionPercentage}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pipeline Flow - Single Balanced Row */}
|
{/* 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) => {
|
{stages.map((stage, index) => {
|
||||||
const Icon = stage.icon;
|
const Icon = stage.icon;
|
||||||
const count = data[stage.key as keyof PipelineData];
|
const count = data[stage.key as keyof PipelineData];
|
||||||
const isTransparent = 'transparent' in stage && stage.transparent;
|
const isTransparent = 'transparent' in stage && stage.transparent;
|
||||||
|
const isLast = index === stages.length - 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={stage.key} className="flex items-center">
|
<div key={stage.key} className="flex items-center flex-1">
|
||||||
<Link
|
<Link
|
||||||
to={stage.href}
|
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 ${
|
<div className={`p-2.5 rounded-xl ${
|
||||||
isTransparent
|
isTransparent
|
||||||
@@ -106,7 +112,11 @@ export default function WorkflowPipelineWidget({ data, loading }: WorkflowPipeli
|
|||||||
{loading ? '—' : typeof count === 'number' ? count.toLocaleString() : count}
|
{loading ? '—' : typeof count === 'number' ? count.toLocaleString() : count}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
{index < stages.length - 1 && <ArrowTip />}
|
{!isLast && (
|
||||||
|
<div className="flex-shrink-0 mx-1">
|
||||||
|
<ArrowTip />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export interface WizardData {
|
|||||||
const STEPS: WizardStep[] = [
|
const STEPS: WizardStep[] = [
|
||||||
{ id: 1, title: 'Welcome', description: 'Get started with IGNY8' },
|
{ id: 1, title: 'Welcome', description: 'Get started with IGNY8' },
|
||||||
{ id: 2, title: 'Add Site', description: 'Create your first site' },
|
{ 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: 4, title: 'Keywords', description: 'Add target keywords', isOptional: true },
|
||||||
{ id: 5, title: 'Complete', description: 'You\'re all set!' },
|
{ id: 5, title: 'Complete', description: 'You\'re all set!' },
|
||||||
];
|
];
|
||||||
@@ -146,6 +146,8 @@ export default function OnboardingWizard({ onComplete, onSkip }: OnboardingWizar
|
|||||||
<Step1Welcome
|
<Step1Welcome
|
||||||
onNext={handleNext}
|
onNext={handleNext}
|
||||||
onSkip={handleSkipAll}
|
onSkip={handleSkipAll}
|
||||||
|
currentStep={currentStep}
|
||||||
|
totalSteps={STEPS.length}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 2:
|
case 2:
|
||||||
@@ -156,6 +158,8 @@ export default function OnboardingWizard({ onComplete, onSkip }: OnboardingWizar
|
|||||||
onNext={handleNext}
|
onNext={handleNext}
|
||||||
onBack={handleBack}
|
onBack={handleBack}
|
||||||
setIsLoading={setIsLoading}
|
setIsLoading={setIsLoading}
|
||||||
|
currentStep={currentStep}
|
||||||
|
totalSteps={STEPS.length}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 3:
|
case 3:
|
||||||
@@ -166,6 +170,8 @@ export default function OnboardingWizard({ onComplete, onSkip }: OnboardingWizar
|
|||||||
onNext={handleNext}
|
onNext={handleNext}
|
||||||
onBack={handleBack}
|
onBack={handleBack}
|
||||||
onSkip={handleSkipStep}
|
onSkip={handleSkipStep}
|
||||||
|
currentStep={currentStep}
|
||||||
|
totalSteps={STEPS.length}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 4:
|
case 4:
|
||||||
@@ -176,6 +182,8 @@ export default function OnboardingWizard({ onComplete, onSkip }: OnboardingWizar
|
|||||||
onNext={handleNext}
|
onNext={handleNext}
|
||||||
onBack={handleBack}
|
onBack={handleBack}
|
||||||
onSkip={handleSkipStep}
|
onSkip={handleSkipStep}
|
||||||
|
currentStep={currentStep}
|
||||||
|
totalSteps={STEPS.length}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 5:
|
case 5:
|
||||||
@@ -184,6 +192,8 @@ export default function OnboardingWizard({ onComplete, onSkip }: OnboardingWizar
|
|||||||
data={wizardData}
|
data={wizardData}
|
||||||
onComplete={handleComplete}
|
onComplete={handleComplete}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
currentStep={currentStep}
|
||||||
|
totalSteps={STEPS.length}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
@@ -192,86 +202,58 @@ export default function OnboardingWizard({ onComplete, onSkip }: OnboardingWizar
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-3xl mx-auto">
|
<div className="w-full max-w-6xl mx-auto">
|
||||||
<Card className="w-full bg-white dark:bg-gray-900 rounded-2xl shadow-lg overflow-hidden">
|
{/* Progress Steps - Clean horizontal stepper */}
|
||||||
{/* Header */}
|
<div className="mb-8">
|
||||||
<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 justify-between">
|
{STEPS.map((step, index) => (
|
||||||
<div className="flex items-center gap-3">
|
<React.Fragment key={step.id}>
|
||||||
<div className="size-10 rounded-xl bg-gradient-to-br from-brand-500 to-brand-600 flex items-center justify-center text-white">
|
<div className="flex flex-col items-center">
|
||||||
<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
|
<div
|
||||||
key={step.id}
|
className={`size-12 rounded-full flex items-center justify-center text-base font-semibold transition-all ${
|
||||||
className={`flex items-center ${step.id < STEPS.length ? 'flex-1' : ''}`}
|
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
|
{step.id < currentStep ? (
|
||||||
className={`size-8 rounded-full flex items-center justify-center text-sm font-medium transition-colors ${
|
<CheckCircleIcon className="w-6 h-6" />
|
||||||
step.id < currentStep
|
) : (
|
||||||
? 'bg-success-500 text-white'
|
step.id
|
||||||
: 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'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
{STEPS.map((step) => (
|
|
||||||
<span
|
<span
|
||||||
key={step.id}
|
className={`mt-2 text-sm font-medium text-center ${
|
||||||
className={`text-center ${
|
step.id === currentStep
|
||||||
step.id === currentStep ? 'text-brand-600 dark:text-brand-400 font-medium' : ''
|
? '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}
|
{step.title}
|
||||||
</span>
|
</span>
|
||||||
))}
|
</div>
|
||||||
</div>
|
{index < STEPS.length - 1 && (
|
||||||
</div>
|
<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>
|
||||||
|
</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 */}
|
{/* Step Content */}
|
||||||
<div className="p-6">
|
<div className="p-8 sm:p-10">
|
||||||
{renderStepContent()}
|
{renderStepContent()}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Step 1: Welcome
|
* Step 1: Welcome
|
||||||
* Introduction screen for new users
|
* Redesigned intro with cleaner cards explaining what each step accomplishes
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Button from '../../ui/button/Button';
|
import Button from '../../ui/button/Button';
|
||||||
@@ -9,99 +9,126 @@ import {
|
|||||||
BoltIcon,
|
BoltIcon,
|
||||||
FileTextIcon,
|
FileTextIcon,
|
||||||
PlugInIcon,
|
PlugInIcon,
|
||||||
PieChartIcon,
|
CheckCircleIcon,
|
||||||
} from '../../../icons';
|
} from '../../../icons';
|
||||||
|
|
||||||
interface Step1WelcomeProps {
|
interface Step1WelcomeProps {
|
||||||
onNext: () => void;
|
onNext: () => void;
|
||||||
onSkip: () => void;
|
onSkip: () => void;
|
||||||
|
currentStep: number;
|
||||||
|
totalSteps: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FEATURES = [
|
const WIZARD_STEPS = [
|
||||||
{
|
{
|
||||||
icon: <FileTextIcon className="h-5 w-5" />,
|
step: 1,
|
||||||
title: 'AI Content Creation',
|
icon: <FileTextIcon className="h-6 w-6" />,
|
||||||
description: 'Generate high-quality articles with AI assistance',
|
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" />,
|
step: 2,
|
||||||
title: 'WordPress Integration',
|
icon: <PlugInIcon className="h-6 w-6" />,
|
||||||
description: 'Publish directly to your WordPress site',
|
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" />,
|
step: 3,
|
||||||
title: 'Automated Pipeline',
|
icon: <BoltIcon className="h-6 w-6" />,
|
||||||
description: 'Set it and forget it content scheduling',
|
title: 'Add Keywords',
|
||||||
},
|
description: 'Define target keywords for AI content',
|
||||||
{
|
outcome: 'Keywords ready for clustering and ideas',
|
||||||
icon: <PieChartIcon className="h-5 w-5" />,
|
color: 'warning',
|
||||||
title: 'Smart Analytics',
|
optional: true,
|
||||||
description: 'Track content performance and optimize',
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function Step1Welcome({ onNext, onSkip }: Step1WelcomeProps) {
|
export default function Step1Welcome({ onNext, onSkip, currentStep, totalSteps }: Step1WelcomeProps) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center">
|
<div>
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<div className="mb-8">
|
<div className="text-center mb-10">
|
||||||
<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">
|
<h1 className="text-3xl sm:text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
<BoltIcon className="h-10 w-10" />
|
|
||||||
</div>
|
|
||||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
|
|
||||||
Welcome to IGNY8
|
Welcome to IGNY8
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-600 dark:text-gray-400 max-w-md mx-auto">
|
<p className="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
|
||||||
Your complete AI-powered content creation and publishing platform.
|
Let's set up your AI-powered content pipeline in just a few steps.
|
||||||
Let's get you set up in just a few minutes.
|
You'll be creating SEO-optimized content in minutes.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Features Grid */}
|
{/* What You'll Accomplish - Horizontal Cards */}
|
||||||
<div className="grid grid-cols-2 gap-4 mb-8">
|
<div className="mb-10">
|
||||||
{FEATURES.map((feature, index) => (
|
<h2 className="text-base font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-5 text-center">
|
||||||
<div
|
What we'll set up together
|
||||||
key={index}
|
</h2>
|
||||||
className="p-4 rounded-xl bg-gray-50 dark:bg-gray-800 text-left"
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
>
|
{WIZARD_STEPS.map((item) => (
|
||||||
<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">
|
<div
|
||||||
{feature.icon}
|
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>
|
</div>
|
||||||
<h3 className="font-semibold text-gray-900 dark:text-white text-sm mb-1">
|
))}
|
||||||
{feature.title}
|
</div>
|
||||||
</h3>
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
{feature.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* What's Next */}
|
{/* Time Estimate */}
|
||||||
<div className="bg-brand-50 dark:bg-brand-900/20 rounded-xl p-4 mb-6">
|
<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">
|
||||||
<h3 className="font-semibold text-brand-900 dark:text-brand-100 mb-2">
|
<p className="text-base text-brand-700 dark:text-brand-300">
|
||||||
What we'll do together:
|
<span className="font-semibold">⏱️ Estimated time:</span> 3-5 minutes
|
||||||
</h3>
|
</p>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* 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
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
tone="neutral"
|
tone="neutral"
|
||||||
|
size="md"
|
||||||
onClick={onSkip}
|
onClick={onSkip}
|
||||||
>
|
>
|
||||||
Skip for now
|
Skip for now
|
||||||
</Button>
|
</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
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
|
size="md"
|
||||||
onClick={onNext}
|
onClick={onNext}
|
||||||
endIcon={<ArrowRightIcon className="w-4 h-4" />}
|
endIcon={<ArrowRightIcon className="w-5 h-5" />}
|
||||||
>
|
>
|
||||||
Let's Get Started
|
Let's Get Started
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import {
|
|||||||
PageIcon,
|
PageIcon,
|
||||||
GridIcon,
|
GridIcon,
|
||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
|
LockIcon,
|
||||||
|
PlusIcon,
|
||||||
} from '../../../icons';
|
} from '../../../icons';
|
||||||
import {
|
import {
|
||||||
fetchIndustries,
|
fetchIndustries,
|
||||||
@@ -32,6 +34,8 @@ interface Step2AddSiteProps {
|
|||||||
onNext: () => void;
|
onNext: () => void;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
setIsLoading: (loading: boolean) => void;
|
setIsLoading: (loading: boolean) => void;
|
||||||
|
currentStep: number;
|
||||||
|
totalSteps: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Step2AddSite({
|
export default function Step2AddSite({
|
||||||
@@ -39,7 +43,9 @@ export default function Step2AddSite({
|
|||||||
updateData,
|
updateData,
|
||||||
onNext,
|
onNext,
|
||||||
onBack,
|
onBack,
|
||||||
setIsLoading
|
setIsLoading,
|
||||||
|
currentStep,
|
||||||
|
totalSteps,
|
||||||
}: Step2AddSiteProps) {
|
}: Step2AddSiteProps) {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
@@ -49,6 +55,9 @@ export default function Step2AddSite({
|
|||||||
const [isCreating, setIsCreating] = useState(false);
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Check if site is already created (locked state)
|
||||||
|
const isSiteCreated = !!data.createdSiteId;
|
||||||
|
|
||||||
// Load industries on mount
|
// Load industries on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadIndustries = async () => {
|
const loadIndustries = async () => {
|
||||||
@@ -56,6 +65,12 @@ export default function Step2AddSite({
|
|||||||
setLoadingIndustries(true);
|
setLoadingIndustries(true);
|
||||||
const response = await fetchIndustries();
|
const response = await fetchIndustries();
|
||||||
setIndustries(response.industries || []);
|
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) {
|
} catch (err: any) {
|
||||||
if (err?.status !== 429) {
|
if (err?.status !== 429) {
|
||||||
console.error('Failed to load industries:', err);
|
console.error('Failed to load industries:', err);
|
||||||
@@ -65,7 +80,7 @@ export default function Step2AddSite({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
loadIndustries();
|
loadIndustries();
|
||||||
}, []);
|
}, [data.industrySlug]);
|
||||||
|
|
||||||
// Get available sectors for selected industry
|
// Get available sectors for selected industry
|
||||||
const availableSectors = useMemo(() => {
|
const availableSectors = useMemo(() => {
|
||||||
@@ -74,6 +89,7 @@ export default function Step2AddSite({
|
|||||||
}, [selectedIndustry]);
|
}, [selectedIndustry]);
|
||||||
|
|
||||||
const handleIndustrySelect = (industry: Industry) => {
|
const handleIndustrySelect = (industry: Industry) => {
|
||||||
|
if (isSiteCreated) return; // Don't allow changes if site is created
|
||||||
setSelectedIndustry(industry);
|
setSelectedIndustry(industry);
|
||||||
updateData({
|
updateData({
|
||||||
industryId: industry.id || null,
|
industryId: industry.id || null,
|
||||||
@@ -83,6 +99,7 @@ export default function Step2AddSite({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSectorToggle = (sectorSlug: string) => {
|
const handleSectorToggle = (sectorSlug: string) => {
|
||||||
|
if (isSiteCreated) return; // Don't allow changes if site is created
|
||||||
const newSectors = data.selectedSectors.includes(sectorSlug)
|
const newSectors = data.selectedSectors.includes(sectorSlug)
|
||||||
? data.selectedSectors.filter(s => s !== sectorSlug)
|
? data.selectedSectors.filter(s => s !== sectorSlug)
|
||||||
: [...data.selectedSectors, sectorSlug];
|
: [...data.selectedSectors, sectorSlug];
|
||||||
@@ -139,10 +156,33 @@ export default function Step2AddSite({
|
|||||||
// Update wizard data with created site
|
// Update wizard data with created site
|
||||||
updateData({ createdSiteId: newSite.id });
|
updateData({ createdSiteId: newSite.id });
|
||||||
|
|
||||||
toast.success(`Site "${data.siteName.trim()}" created successfully!`);
|
toast.success(`Site "${data.siteName.trim()}" added successfully!`);
|
||||||
onNext();
|
onNext();
|
||||||
} catch (err: any) {
|
} 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 {
|
} finally {
|
||||||
setIsCreating(false);
|
setIsCreating(false);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -151,11 +191,11 @@ export default function Step2AddSite({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-6">
|
<div className="mb-8">
|
||||||
<h2 className="text-xl font-bold text-gray-900 dark:text-white mb-2">
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
|
||||||
Add Your First Site
|
Add Your First Site
|
||||||
</h2>
|
</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.
|
We'll set up your site with optimized defaults for automated content publishing.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -168,77 +208,103 @@ export default function Step2AddSite({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Site Name */}
|
{/* Site Added Lock Notice */}
|
||||||
<div className="mb-4">
|
{isSiteCreated && (
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<Alert
|
||||||
Site Name *
|
variant="success"
|
||||||
</label>
|
title="Site Added"
|
||||||
<input
|
message={`Your site "${data.siteName}" has been added. To edit site details, go to Site Settings after completing the wizard.`}
|
||||||
type="text"
|
className="mb-8"
|
||||||
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"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{/* Website URL */}
|
{/* Form Fields - 3 in a row */}
|
||||||
<div className="mb-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
{/* Site Name */}
|
||||||
Website URL (optional)
|
<div>
|
||||||
</label>
|
<label className="block text-base font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
<div className="relative">
|
Site Name *
|
||||||
<PageIcon className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={data.domain}
|
value={data.siteName}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => updateData({ domain: e.target.value })}
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => updateData({ siteName: e.target.value })}
|
||||||
placeholder="https://mysite.com"
|
placeholder="My Awesome Blog"
|
||||||
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"
|
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>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Industry Selection */}
|
{/* Website URL */}
|
||||||
<div className="mb-4">
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<label className="block text-base font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
Industry *
|
Website URL
|
||||||
</label>
|
</label>
|
||||||
{loadingIndustries ? (
|
<div className="relative">
|
||||||
<div className="text-sm text-gray-500">Loading industries...</div>
|
<PageIcon className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||||||
) : (
|
<input
|
||||||
<SelectDropdown
|
type="text"
|
||||||
options={industries.map(i => ({ value: i.slug, label: i.name }))}
|
value={data.domain}
|
||||||
value={selectedIndustry?.slug || ''}
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => updateData({ domain: e.target.value })}
|
||||||
onChange={(value) => {
|
placeholder="https://mysite.com"
|
||||||
const industry = industries.find(i => i.slug === value);
|
disabled={isSiteCreated}
|
||||||
if (industry) handleIndustrySelect(industry);
|
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' : ''}`}
|
||||||
}}
|
/>
|
||||||
placeholder="Select an industry"
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Sector Selection */}
|
{/* Sector Selection */}
|
||||||
{selectedIndustry && availableSectors.length > 0 && (
|
{selectedIndustry && availableSectors.length > 0 && (
|
||||||
<div className="mb-6">
|
<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>
|
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>
|
</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) => {
|
{availableSectors.map((sector: Sector) => {
|
||||||
const isSelected = data.selectedSectors.includes(sector.slug);
|
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 (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
key={sector.slug}
|
key={sector.slug}
|
||||||
tone={isSelected ? 'success' : 'neutral'}
|
tone={isSelected ? 'success' : 'neutral'}
|
||||||
variant="soft"
|
variant="soft"
|
||||||
className={`cursor-pointer transition-all ${
|
size="md"
|
||||||
isSelected ? 'ring-2 ring-success-500' : 'hover:bg-gray-200 dark:hover:bg-gray-700'
|
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">
|
<span onClick={() => canToggle && handleSectorToggle(sector.slug)} className="flex items-center gap-1.5">
|
||||||
{isSelected && <CheckCircleIcon className="w-3 h-3 mr-1" />}
|
{isSelected && <CheckCircleIcon className="w-4 h-4" />}
|
||||||
{sector.name}
|
{sector.name}
|
||||||
</span>
|
</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -246,28 +312,43 @@ export default function Step2AddSite({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
{data.selectedSectors.length > 0 && (
|
{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
|
{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>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Defaults Info */}
|
{/* Defaults Info - 2 columns */}
|
||||||
<Card className="p-4 bg-brand-50 dark:bg-brand-900/20 border-brand-200 dark:border-brand-800 mb-6">
|
<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-3">
|
<div className="flex items-start gap-4">
|
||||||
<GridIcon className="w-5 h-5 text-brand-600 dark:text-brand-400 mt-0.5" />
|
<GridIcon className="w-6 h-6 text-brand-600 dark:text-brand-400 mt-0.5 flex-shrink-0" />
|
||||||
<div>
|
<div className="flex-1">
|
||||||
<h4 className="font-medium text-brand-900 dark:text-brand-100 text-sm mb-1">
|
<h4 className="font-semibold text-brand-900 dark:text-brand-100 text-base mb-3">
|
||||||
Optimized Defaults Applied
|
Optimized Defaults Applied
|
||||||
</h4>
|
</h4>
|
||||||
<ul className="text-xs text-brand-700 dark:text-brand-300 space-y-0.5">
|
<div className="grid grid-cols-2 gap-x-8 gap-y-2 text-base text-brand-700 dark:text-brand-300">
|
||||||
<li>• Auto-approval enabled</li>
|
<div className="flex items-center gap-2">
|
||||||
<li>• Auto-publish to site enabled</li>
|
<CheckCircleIcon className="w-4 h-4 text-brand-500" />
|
||||||
<li>• 3 articles/day limit</li>
|
<span>Auto-approval enabled</span>
|
||||||
<li>• Publishing Mon-Fri at 9am, 2pm, 6pm</li>
|
</div>
|
||||||
</ul>
|
<div className="flex items-center gap-2">
|
||||||
<p className="text-xs text-brand-600 dark:text-brand-400 mt-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.
|
You can customize these in Site Settings anytime.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -275,23 +356,39 @@ export default function Step2AddSite({
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* 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
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
tone="neutral"
|
tone="neutral"
|
||||||
|
size="md"
|
||||||
onClick={onBack}
|
onClick={onBack}
|
||||||
startIcon={<ArrowLeftIcon className="w-4 h-4" />}
|
startIcon={<ArrowLeftIcon className="w-5 h-5" />}
|
||||||
>
|
>
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
variant="primary"
|
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>
|
||||||
onClick={handleCreateSite}
|
</span>
|
||||||
disabled={isCreating || !data.siteName.trim() || !selectedIndustry || data.selectedSectors.length === 0}
|
{isSiteCreated ? (
|
||||||
endIcon={!isCreating ? <ArrowRightIcon className="w-4 h-4" /> : undefined}
|
<Button
|
||||||
>
|
variant="primary"
|
||||||
{isCreating ? 'Creating...' : 'Create Site'}
|
size="md"
|
||||||
</Button>
|
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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Step 3: Connect Integration
|
* 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 React, { useState, useEffect } from 'react';
|
||||||
import Button from '../../ui/button/Button';
|
import Button from '../../ui/button/Button';
|
||||||
@@ -15,6 +16,7 @@ import {
|
|||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
TimeIcon,
|
TimeIcon,
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
|
BoxCubeIcon,
|
||||||
} from '../../../icons';
|
} from '../../../icons';
|
||||||
import { integrationApi } from '../../../services/integration.api';
|
import { integrationApi } from '../../../services/integration.api';
|
||||||
import { useToast } from '../../ui/toast/ToastContainer';
|
import { useToast } from '../../ui/toast/ToastContainer';
|
||||||
@@ -26,17 +28,50 @@ interface Step3ConnectIntegrationProps {
|
|||||||
onNext: () => void;
|
onNext: () => void;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
onSkip: () => 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({
|
export default function Step3ConnectIntegration({
|
||||||
data,
|
data,
|
||||||
updateData,
|
updateData,
|
||||||
onNext,
|
onNext,
|
||||||
onBack,
|
onBack,
|
||||||
onSkip
|
onSkip,
|
||||||
|
currentStep,
|
||||||
|
totalSteps,
|
||||||
}: Step3ConnectIntegrationProps) {
|
}: Step3ConnectIntegrationProps) {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
|
const [selectedPlatform, setSelectedPlatform] = useState<PlatformType | null>(null);
|
||||||
const [apiKey, setApiKey] = useState<string>('');
|
const [apiKey, setApiKey] = useState<string>('');
|
||||||
const [isTesting, setIsTesting] = useState(false);
|
const [isTesting, setIsTesting] = useState(false);
|
||||||
const [testResult, setTestResult] = useState<'success' | 'failed' | null>(null);
|
const [testResult, setTestResult] = useState<'success' | 'failed' | null>(null);
|
||||||
@@ -82,7 +117,6 @@ export default function Step3ConnectIntegration({
|
|||||||
setTestResult(null);
|
setTestResult(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// First get the WordPress integration ID
|
|
||||||
const integrations = await integrationApi.getSiteIntegrations(data.createdSiteId);
|
const integrations = await integrationApi.getSiteIntegrations(data.createdSiteId);
|
||||||
const wpIntegration = integrations.find((i) => i.platform === 'wordpress');
|
const wpIntegration = integrations.find((i) => i.platform === 'wordpress');
|
||||||
|
|
||||||
@@ -119,7 +153,7 @@ export default function Step3ConnectIntegration({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
tone="brand"
|
tone="brand"
|
||||||
size="sm"
|
size="sm"
|
||||||
startIcon={<DownloadIcon className="w-3 h-3" />}
|
startIcon={<DownloadIcon className="w-4 h-4" />}
|
||||||
>
|
>
|
||||||
Download Plugin
|
Download Plugin
|
||||||
</Button>
|
</Button>
|
||||||
@@ -135,11 +169,6 @@ export default function Step3ConnectIntegration({
|
|||||||
title: 'Activate & Configure',
|
title: 'Activate & Configure',
|
||||||
description: 'Activate the plugin and enter your API key',
|
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) {
|
if (isLoading) {
|
||||||
@@ -152,143 +181,185 @@ export default function Step3ConnectIntegration({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-6">
|
<div className="mb-8">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-3 mb-3">
|
||||||
<h2 className="text-xl font-bold text-gray-900 dark:text-white">
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
Connect WordPress
|
Connect Your Site
|
||||||
</h2>
|
</h2>
|
||||||
<Badge tone="neutral" variant="soft">Optional</Badge>
|
<Badge tone="neutral" variant="soft" size="sm">Optional</Badge>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-gray-600 dark:text-gray-400">
|
<p className="text-base text-gray-600 dark:text-gray-400">
|
||||||
Install our WordPress plugin to enable automatic publishing. You can do this later if you prefer.
|
Select your platform to enable automatic publishing. You can do this later if you prefer.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* API Key Section */}
|
{/* Platform Selection */}
|
||||||
{apiKey && (
|
<div className="grid grid-cols-3 gap-4 mb-8">
|
||||||
<Card className="p-4 mb-6 bg-gray-50 dark:bg-gray-800">
|
{PLATFORMS.map((platform) => (
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<button
|
||||||
Your API Key
|
key={platform.id}
|
||||||
</label>
|
onClick={() => platform.available && setSelectedPlatform(platform.id)}
|
||||||
<div className="flex items-center gap-2">
|
disabled={!platform.available}
|
||||||
<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">
|
className={`relative p-5 rounded-xl border-2 text-left transition-all ${
|
||||||
{apiKey}
|
selectedPlatform === platform.id
|
||||||
</code>
|
? 'border-brand-500 bg-brand-50 dark:bg-brand-900/20'
|
||||||
<Button
|
: platform.available
|
||||||
variant="outline"
|
? 'border-gray-200 dark:border-gray-700 hover:border-brand-300 dark:hover:border-brand-700 bg-white dark:bg-gray-800'
|
||||||
size="sm"
|
: 'border-gray-100 dark:border-gray-800 bg-gray-50 dark:bg-gray-900 opacity-60 cursor-not-allowed'
|
||||||
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"
|
|
||||||
>
|
>
|
||||||
<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">
|
{platform.comingSoon && (
|
||||||
{item.step}
|
<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">
|
||||||
</div>
|
Coming Soon
|
||||||
<div className="flex-1">
|
</span>
|
||||||
<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>
|
|
||||||
)}
|
)}
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
{/* Test Connection */}
|
{/* WordPress Setup (shown when WordPress is selected) */}
|
||||||
<Card className="p-4 mb-6 border-2 border-dashed border-gray-200 dark:border-gray-700">
|
{selectedPlatform === 'wordpress' && (
|
||||||
<div className="flex items-center justify-between">
|
<>
|
||||||
<div className="flex items-center gap-3">
|
{/* API Key Section */}
|
||||||
<div className={`size-10 rounded-lg flex items-center justify-center ${
|
{apiKey && (
|
||||||
testResult === 'success'
|
<Card className="p-5 mb-6 bg-gray-50 dark:bg-gray-800">
|
||||||
? 'bg-success-100 dark:bg-success-900/50 text-success-600 dark:text-success-400'
|
<label className="block text-base font-medium text-gray-700 dark:text-gray-300 mb-3">
|
||||||
: testResult === 'failed'
|
Your API Key
|
||||||
? 'bg-error-100 dark:bg-error-900/50 text-error-600 dark:text-error-400'
|
</label>
|
||||||
: 'bg-gray-100 dark:bg-gray-800 text-gray-500'
|
<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">
|
||||||
{testResult === 'success' ? (
|
{apiKey}
|
||||||
<CheckCircleIcon className="w-5 h-5" />
|
</code>
|
||||||
) : (
|
<Button
|
||||||
<PlugInIcon className="w-5 h-5" />
|
variant="outline"
|
||||||
)}
|
size="md"
|
||||||
</div>
|
onClick={handleCopyApiKey}
|
||||||
<div>
|
className="flex-shrink-0"
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">
|
startIcon={<CopyIcon className="w-4 h-4" />}
|
||||||
Connection Status
|
>
|
||||||
</h4>
|
Copy
|
||||||
<p className="text-xs text-gray-500">
|
</Button>
|
||||||
{testResult === 'success'
|
</div>
|
||||||
? 'Successfully connected to WordPress'
|
</Card>
|
||||||
: testResult === 'failed'
|
)}
|
||||||
? 'Connection failed - check plugin installation'
|
|
||||||
: 'Test the connection after installing the plugin'}
|
{/* Installation Steps */}
|
||||||
</p>
|
<div className="space-y-3 mb-6">
|
||||||
</div>
|
{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>
|
</div>
|
||||||
<Button
|
|
||||||
variant={testResult === 'success' ? 'primary' : 'outline'}
|
{/* Test Connection */}
|
||||||
size="sm"
|
<Card className="p-5 mb-6 border-2 border-dashed border-gray-200 dark:border-gray-700">
|
||||||
onClick={handleTestConnection}
|
<div className="flex items-center justify-between">
|
||||||
disabled={isTesting}
|
<div className="flex items-center gap-4">
|
||||||
startIcon={
|
<div className={`size-12 rounded-xl flex items-center justify-center ${
|
||||||
isTesting ? <TimeIcon className="w-4 h-4 animate-spin" /> :
|
testResult === 'success'
|
||||||
testResult === 'success' ? <CheckCircleIcon className="w-4 h-4" /> :
|
? 'bg-success-100 dark:bg-success-900/50 text-success-600 dark:text-success-400'
|
||||||
<TimeIcon className="w-4 h-4" />
|
: 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'
|
||||||
{isTesting ? 'Testing...' : testResult === 'success' ? 'Connected' : 'Test Connection'}
|
}`}>
|
||||||
</Button>
|
{testResult === 'success' ? (
|
||||||
</div>
|
<CheckCircleIcon className="w-6 h-6" />
|
||||||
</Card>
|
) : (
|
||||||
|
<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 */}
|
{/* Info Alert */}
|
||||||
<Alert
|
<Alert
|
||||||
variant="info"
|
variant="info"
|
||||||
title="Skip for now"
|
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 */}
|
{/* 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
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
tone="neutral"
|
tone="neutral"
|
||||||
|
size="md"
|
||||||
onClick={onBack}
|
onClick={onBack}
|
||||||
startIcon={<ArrowLeftIcon className="w-4 h-4" />}
|
startIcon={<ArrowLeftIcon className="w-5 h-5" />}
|
||||||
>
|
>
|
||||||
Back
|
Back
|
||||||
</Button>
|
</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
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
tone="neutral"
|
tone="neutral"
|
||||||
|
size="md"
|
||||||
onClick={onSkip}
|
onClick={onSkip}
|
||||||
>
|
>
|
||||||
Skip for now
|
Skip for now
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
|
size="md"
|
||||||
onClick={onNext}
|
onClick={onNext}
|
||||||
endIcon={<ArrowRightIcon className="w-4 h-4" />}
|
endIcon={<ArrowRightIcon className="w-5 h-5" />}
|
||||||
>
|
>
|
||||||
Continue
|
Continue
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* Step 4: Add Keywords
|
* Step 4: Add Keywords
|
||||||
* Initial keyword input for content pipeline
|
* 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 Button from '../../ui/button/Button';
|
||||||
import IconButton from '../../ui/button/IconButton';
|
import IconButton from '../../ui/button/IconButton';
|
||||||
import InputField from '../../form/input/InputField';
|
|
||||||
import { Card } from '../../ui/card';
|
import { Card } from '../../ui/card';
|
||||||
import Badge from '../../ui/badge/Badge';
|
import Badge from '../../ui/badge/Badge';
|
||||||
import Alert from '../../ui/alert/Alert';
|
import Alert from '../../ui/alert/Alert';
|
||||||
@@ -15,8 +15,18 @@ import {
|
|||||||
ListIcon,
|
ListIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
CloseIcon,
|
CloseIcon,
|
||||||
|
BoltIcon,
|
||||||
|
PencilIcon,
|
||||||
|
CheckCircleIcon,
|
||||||
} from '../../../icons';
|
} from '../../../icons';
|
||||||
import { createKeyword } from '../../../services/api';
|
import {
|
||||||
|
createKeyword,
|
||||||
|
fetchSeedKeywords,
|
||||||
|
addSeedKeywordsToWorkflow,
|
||||||
|
fetchSiteSectors,
|
||||||
|
fetchIndustries,
|
||||||
|
SeedKeyword,
|
||||||
|
} from '../../../services/api';
|
||||||
import { useToast } from '../../ui/toast/ToastContainer';
|
import { useToast } from '../../ui/toast/ToastContainer';
|
||||||
import type { WizardData } from '../OnboardingWizard';
|
import type { WizardData } from '../OnboardingWizard';
|
||||||
|
|
||||||
@@ -26,6 +36,24 @@ interface Step4AddKeywordsProps {
|
|||||||
onNext: () => void;
|
onNext: () => void;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
onSkip: () => 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({
|
export default function Step4AddKeywords({
|
||||||
@@ -33,15 +61,183 @@ export default function Step4AddKeywords({
|
|||||||
updateData,
|
updateData,
|
||||||
onNext,
|
onNext,
|
||||||
onBack,
|
onBack,
|
||||||
onSkip
|
onSkip,
|
||||||
|
currentStep,
|
||||||
|
totalSteps,
|
||||||
}: Step4AddKeywordsProps) {
|
}: Step4AddKeywordsProps) {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
|
const [mode, setMode] = useState<KeywordMode>(null);
|
||||||
const [keywords, setKeywords] = useState<string[]>([]);
|
const [keywords, setKeywords] = useState<string[]>([]);
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
const [isAdding, setIsAdding] = useState(false);
|
const [isAdding, setIsAdding] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
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 handleAddKeyword = () => {
|
||||||
const keyword = inputValue.trim();
|
const keyword = inputValue.trim();
|
||||||
if (!keyword) return;
|
if (!keyword) return;
|
||||||
@@ -126,66 +322,334 @@ export default function Step4AddKeywords({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const SUGGESTIONS = [
|
// Mode selection view
|
||||||
'best [product] for [use case]',
|
if (mode === null) {
|
||||||
'how to [action] with [tool]',
|
return (
|
||||||
'[topic] guide for beginners',
|
<div>
|
||||||
'[industry] trends 2025',
|
<div className="mb-8">
|
||||||
'[problem] solutions for [audience]',
|
<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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-6">
|
<div className="mb-8">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<button
|
||||||
<h2 className="text-xl font-bold text-gray-900 dark:text-white">
|
onClick={() => setMode(null)}
|
||||||
Add Target Keywords
|
className="flex items-center gap-2 text-base text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 mb-4"
|
||||||
</h2>
|
>
|
||||||
<Badge tone="neutral" variant="soft">Optional</Badge>
|
<ArrowLeftIcon className="w-4 h-4" />
|
||||||
</div>
|
<span>Back to options</span>
|
||||||
<p className="text-gray-600 dark:text-gray-400">
|
</button>
|
||||||
Add keywords to start your content pipeline. These will be used to generate content ideas and articles.
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<Alert variant="error" title="Error" message={error} />
|
<Alert variant="error" title="Error" message={error} className="mb-6" />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Keyword Input */}
|
{/* Keyword Input */}
|
||||||
<div className="mb-4">
|
<div className="mb-6">
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
<label className="block text-base font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
Enter keywords (press Enter or paste multiple)
|
Enter keywords (press Enter or paste multiple)
|
||||||
</label>
|
</label>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-3">
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1">
|
||||||
<ListIcon className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
<ListIcon className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
|
||||||
<InputField
|
<input
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={(e) => setInputValue(e.target.value)}
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onPaste={handlePaste}
|
||||||
placeholder="Enter a keyword..."
|
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>
|
</div>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
size="md"
|
||||||
onClick={handleAddKeyword}
|
onClick={handleAddKeyword}
|
||||||
disabled={!inputValue.trim()}
|
disabled={!inputValue.trim()}
|
||||||
icon={<PlusIcon className="w-4 h-4" />}
|
icon={<PlusIcon className="w-5 h-5" />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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
|
Tip: Paste a comma-separated list or one keyword per line
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Keywords List */}
|
{/* 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 ? (
|
{keywords.length === 0 ? (
|
||||||
<div className="text-center py-6 text-gray-500 dark:text-gray-400">
|
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
||||||
<ListIcon className="w-8 h-8 mx-auto mb-2 opacity-50" />
|
<ListIcon className="w-10 h-10 mx-auto mb-3 opacity-50" />
|
||||||
<p className="text-sm">No keywords added yet</p>
|
<p className="text-base">No keywords added yet</p>
|
||||||
<p className="text-xs">Start typing or paste keywords above</p>
|
<p className="text-sm">Start typing or paste keywords above</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
@@ -194,11 +658,12 @@ export default function Step4AddKeywords({
|
|||||||
key={keyword}
|
key={keyword}
|
||||||
tone="neutral"
|
tone="neutral"
|
||||||
variant="soft"
|
variant="soft"
|
||||||
className="gap-1 pr-1"
|
size="md"
|
||||||
|
className="gap-2 pr-2 text-base"
|
||||||
>
|
>
|
||||||
{keyword}
|
{keyword}
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<CloseIcon className="w-3 h-3" />}
|
icon={<CloseIcon className="w-4 h-4" />}
|
||||||
onClick={() => handleRemoveKeyword(keyword)}
|
onClick={() => handleRemoveKeyword(keyword)}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="xs"
|
size="xs"
|
||||||
@@ -211,7 +676,7 @@ export default function Step4AddKeywords({
|
|||||||
)}
|
)}
|
||||||
</Card>
|
</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>
|
<span>{keywords.length} keyword{keywords.length !== 1 ? 's' : ''} added</span>
|
||||||
{keywords.length > 0 && (
|
{keywords.length > 0 && (
|
||||||
<Button
|
<Button
|
||||||
@@ -225,53 +690,43 @@ export default function Step4AddKeywords({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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 */}
|
{/* Info Alert */}
|
||||||
<Alert
|
<Alert
|
||||||
variant="info"
|
variant="info"
|
||||||
title="Add keywords later"
|
title="Add more keywords later"
|
||||||
message="You can add more keywords later from the Planner page. The automation will process these keywords and generate content automatically."
|
message="You can add more keywords from the Planner page. The automation will process these keywords and generate content automatically."
|
||||||
|
className="mb-6"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* 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
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
tone="neutral"
|
tone="neutral"
|
||||||
onClick={onBack}
|
size="md"
|
||||||
startIcon={<ArrowLeftIcon className="w-4 h-4" />}
|
onClick={() => setMode(null)}
|
||||||
|
startIcon={<ArrowLeftIcon className="w-5 h-5" />}
|
||||||
>
|
>
|
||||||
Back
|
Back
|
||||||
</Button>
|
</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
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
tone="neutral"
|
tone="neutral"
|
||||||
|
size="md"
|
||||||
onClick={onSkip}
|
onClick={onSkip}
|
||||||
>
|
>
|
||||||
Skip for now
|
Skip for now
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
|
size="md"
|
||||||
onClick={handleSubmitKeywords}
|
onClick={handleSubmitKeywords}
|
||||||
disabled={isAdding || keywords.length === 0}
|
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' : ''}`}
|
{isAdding ? 'Adding...' : `Add ${keywords.length} Keyword${keywords.length !== 1 ? 's' : ''}`}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,167 +1,244 @@
|
|||||||
/**
|
/**
|
||||||
* Step 5: Complete
|
* 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 Button from '../../ui/button/Button';
|
||||||
import { Card } from '../../ui/card';
|
import { Card } from '../../ui/card';
|
||||||
|
import Badge from '../../ui/badge/Badge';
|
||||||
import {
|
import {
|
||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
ArrowRightIcon,
|
ArrowRightIcon,
|
||||||
BoltIcon,
|
BoltIcon,
|
||||||
FileTextIcon,
|
SettingsIcon,
|
||||||
PieChartIcon,
|
ListIcon,
|
||||||
BoxCubeIcon,
|
|
||||||
} from '../../../icons';
|
} from '../../../icons';
|
||||||
|
import { fetchKeywords } from '../../../services/api';
|
||||||
import type { WizardData } from '../OnboardingWizard';
|
import type { WizardData } from '../OnboardingWizard';
|
||||||
|
|
||||||
interface Step5CompleteProps {
|
interface Step5CompleteProps {
|
||||||
data: WizardData;
|
data: WizardData;
|
||||||
onComplete: () => void;
|
onComplete: () => void;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
currentStep: number;
|
||||||
|
totalSteps: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Step5Complete({
|
export default function Step5Complete({
|
||||||
data,
|
data,
|
||||||
onComplete,
|
onComplete,
|
||||||
isLoading
|
isLoading,
|
||||||
|
currentStep,
|
||||||
|
totalSteps,
|
||||||
}: Step5CompleteProps) {
|
}: Step5CompleteProps) {
|
||||||
const NEXT_STEPS = [
|
const navigate = useNavigate();
|
||||||
{
|
const [keywordCount, setKeywordCount] = useState<number>(0);
|
||||||
icon: <BoltIcon className="w-5 h-5" />,
|
const [loadingCount, setLoadingCount] = useState(true);
|
||||||
title: 'Run Automation',
|
|
||||||
description: 'Start your content pipeline to generate articles',
|
// Fetch actual keyword count from planner for this site
|
||||||
link: data.createdSiteId ? `/sites/${data.createdSiteId}/automation` : '/automation',
|
useEffect(() => {
|
||||||
},
|
const loadKeywordCount = async () => {
|
||||||
{
|
if (!data.createdSiteId) {
|
||||||
icon: <FileTextIcon className="w-5 h-5" />,
|
setLoadingCount(false);
|
||||||
title: 'Add More Keywords',
|
return;
|
||||||
description: 'Expand your content strategy with more target keywords',
|
}
|
||||||
link: '/planner/keywords',
|
|
||||||
},
|
try {
|
||||||
{
|
const response = await fetchKeywords({
|
||||||
icon: <PieChartIcon className="w-5 h-5" />,
|
site_id: data.createdSiteId,
|
||||||
title: 'View Dashboard',
|
page_size: 1, // We only need the count, not the data
|
||||||
description: 'Monitor your content pipeline and metrics',
|
});
|
||||||
link: '/dashboard',
|
setKeywordCount(response.count || 0);
|
||||||
},
|
} catch (err) {
|
||||||
{
|
console.error('Failed to fetch keyword count:', err);
|
||||||
icon: <BoxCubeIcon className="w-5 h-5" />,
|
setKeywordCount(data.keywordsCount || 0); // Fallback to wizard data
|
||||||
title: 'Customize Settings',
|
} finally {
|
||||||
description: 'Fine-tune publishing schedules and preferences',
|
setLoadingCount(false);
|
||||||
link: data.createdSiteId ? `/sites/${data.createdSiteId}/settings` : '/account/settings',
|
}
|
||||||
},
|
};
|
||||||
];
|
|
||||||
|
loadKeywordCount();
|
||||||
|
}, [data.createdSiteId, data.keywordsCount]);
|
||||||
|
|
||||||
|
const handleNavigate = (path: string) => {
|
||||||
|
navigate(path);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="text-center">
|
<div>
|
||||||
{/* Success Animation */}
|
{/* Header - matches other steps */}
|
||||||
<div className="mb-6">
|
<div className="mb-8">
|
||||||
<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">
|
<div className="flex items-center gap-4">
|
||||||
<CheckCircleIcon className="h-10 w-10" />
|
<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>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Summary */}
|
{/* 2-Column Layout: Summary + Next Steps */}
|
||||||
<Card className="p-4 mb-6 bg-gray-50 dark:bg-gray-800 text-left">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||||
<h3 className="font-semibold text-gray-900 dark:text-white mb-3">
|
{/* Left Column - Configuration Summary */}
|
||||||
What we set up:
|
<Card className="p-5 bg-gray-50 dark:bg-gray-800/50 border border-gray-100 dark:border-gray-700">
|
||||||
</h3>
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||||
<ul className="space-y-2">
|
Configuration Summary
|
||||||
<li className="flex items-center gap-2 text-sm">
|
</h3>
|
||||||
<CheckCircleIcon className="w-4 h-4 text-success-500" />
|
<div className="space-y-3">
|
||||||
<span className="text-gray-700 dark:text-gray-300">
|
<div className="flex items-center justify-between py-2 border-b border-gray-200 dark:border-gray-700">
|
||||||
Site: <span className="font-medium">{data.siteName || 'Your Site'}</span>
|
<div className="flex items-center gap-3">
|
||||||
</span>
|
<CheckCircleIcon className="w-5 h-5 text-success-500 flex-shrink-0" />
|
||||||
</li>
|
<span className="text-base text-gray-700 dark:text-gray-300">Site Added</span>
|
||||||
{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}
|
|
||||||
</div>
|
</div>
|
||||||
<h4 className="font-medium text-gray-900 dark:text-white text-sm">
|
<span className="text-base font-medium text-gray-900 dark:text-white truncate max-w-[150px]">
|
||||||
{step.title}
|
{data.siteName || 'Your Site'}
|
||||||
</h4>
|
</span>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
{step.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* CTA */}
|
{/* Footer Actions */}
|
||||||
<Button
|
<div className="flex items-center justify-between pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||||
variant="primary"
|
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
size="lg"
|
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>
|
||||||
onClick={onComplete}
|
</span>
|
||||||
disabled={isLoading}
|
<Button
|
||||||
fullWidth
|
variant="primary"
|
||||||
endIcon={!isLoading ? <ArrowRightIcon className="w-4 h-4" /> : undefined}
|
size="lg"
|
||||||
>
|
onClick={onComplete}
|
||||||
{isLoading ? 'Loading...' : 'Go to Dashboard'}
|
disabled={isLoading}
|
||||||
</Button>
|
endIcon={!isLoading ? <ArrowRightIcon className="w-5 h-5" /> : undefined}
|
||||||
|
>
|
||||||
|
{isLoading ? 'Loading...' : 'Go to Dashboard'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ import AIOperationsWidget, { AIOperationsData } from "../../components/dashboard
|
|||||||
import RecentActivityWidget, { ActivityItem } from "../../components/dashboard/RecentActivityWidget";
|
import RecentActivityWidget, { ActivityItem } from "../../components/dashboard/RecentActivityWidget";
|
||||||
import ContentVelocityWidget, { ContentVelocityData } from "../../components/dashboard/ContentVelocityWidget";
|
import ContentVelocityWidget, { ContentVelocityData } from "../../components/dashboard/ContentVelocityWidget";
|
||||||
import AutomationStatusWidget, { AutomationData } from "../../components/dashboard/AutomationStatusWidget";
|
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() {
|
export default function Home() {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
@@ -37,7 +41,7 @@ export default function Home() {
|
|||||||
const { activeSector } = useSectorStore();
|
const { activeSector } = useSectorStore();
|
||||||
const { isGuideDismissed, showGuide, loadFromBackend } = useOnboardingStore();
|
const { isGuideDismissed, showGuide, loadFromBackend } = useOnboardingStore();
|
||||||
const { user } = useAuthStore();
|
const { user } = useAuthStore();
|
||||||
const { loadBalance } = useBillingStore();
|
const { balance, loadBalance } = useBillingStore();
|
||||||
const { setPageInfo } = usePageContext();
|
const { setPageInfo } = usePageContext();
|
||||||
|
|
||||||
// Core state
|
// Core state
|
||||||
@@ -46,6 +50,7 @@ export default function Home() {
|
|||||||
const [siteFilter, setSiteFilter] = useState<'all' | number>('all');
|
const [siteFilter, setSiteFilter] = useState<'all' | number>('all');
|
||||||
const [showAddSite, setShowAddSite] = useState(false);
|
const [showAddSite, setShowAddSite] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [subscription, setSubscription] = useState<Subscription | null>(null);
|
||||||
|
|
||||||
// Dashboard data state
|
// Dashboard data state
|
||||||
const [attentionItems, setAttentionItems] = useState<AttentionItem[]>([]);
|
const [attentionItems, setAttentionItems] = useState<AttentionItem[]>([]);
|
||||||
@@ -107,9 +112,22 @@ export default function Home() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadSites();
|
loadSites();
|
||||||
loadBalance();
|
loadBalance();
|
||||||
|
loadSubscription();
|
||||||
loadFromBackend().catch(() => {});
|
loadFromBackend().catch(() => {});
|
||||||
}, [loadFromBackend, loadBalance]);
|
}, [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
|
// Load active site if not set
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeSite && sites.length > 0) {
|
if (!activeSite && sites.length > 0) {
|
||||||
@@ -343,13 +361,38 @@ export default function Home() {
|
|||||||
onDismiss={handleDismissAttention}
|
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} />
|
<WorkflowPipelineWidget data={pipelineData} loading={loading} />
|
||||||
|
|
||||||
{/* Row 2: Workflow Guide (full width) */}
|
{/* Row 3: Quick Actions (full width) */}
|
||||||
<QuickActionsWidget />
|
<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">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
||||||
<AIOperationsWidget
|
<AIOperationsWidget
|
||||||
data={aiOperations}
|
data={aiOperations}
|
||||||
@@ -359,7 +402,7 @@ export default function Home() {
|
|||||||
<RecentActivityWidget activities={recentActivity} loading={loading} />
|
<RecentActivityWidget activities={recentActivity} loading={loading} />
|
||||||
</div>
|
</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">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
||||||
<ContentVelocityWidget data={contentVelocity} loading={loading} />
|
<ContentVelocityWidget data={contentVelocity} loading={loading} />
|
||||||
<AutomationStatusWidget
|
<AutomationStatusWidget
|
||||||
|
|||||||
@@ -1,15 +1,27 @@
|
|||||||
/**
|
/**
|
||||||
* Setup Wizard Page
|
* 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
|
* Can be accessed anytime, not just for new users
|
||||||
*/
|
*/
|
||||||
|
import { useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import OnboardingWizard from '../../components/onboarding/OnboardingWizard';
|
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';
|
import { ShootingStarIcon } from '../../icons';
|
||||||
|
|
||||||
export default function SetupWizard() {
|
export default function SetupWizard() {
|
||||||
const navigate = useNavigate();
|
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 = () => {
|
const handleComplete = () => {
|
||||||
navigate('/dashboard');
|
navigate('/dashboard');
|
||||||
@@ -20,19 +32,15 @@ export default function SetupWizard() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<>
|
||||||
<PageHeader
|
<PageMeta
|
||||||
title="Setup Wizard"
|
title="Setup Wizard - IGNY8"
|
||||||
badge={{ icon: <ShootingStarIcon className="w-5 h-5" />, color: 'blue' }}
|
|
||||||
description="Complete guided setup for your site"
|
description="Complete guided setup for your site"
|
||||||
/>
|
/>
|
||||||
|
<OnboardingWizard
|
||||||
<div className="py-4">
|
onComplete={handleComplete}
|
||||||
<OnboardingWizard
|
onSkip={handleSkip}
|
||||||
onComplete={handleComplete}
|
/>
|
||||||
onSkip={handleSkip}
|
</>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user