Update WorkflowGuide component to include industry and sector selection; enhance site creation process with new state management and validation. Refactor SelectDropdown for improved styling and functionality. Add HomepageSiteSelector for better site management in Dashboard.

This commit is contained in:
IGNY8 VPS (Salman)
2025-11-21 03:34:52 +00:00
parent c8adfe06d1
commit 5106f7b200
5 changed files with 1754 additions and 674 deletions

View File

@@ -102,7 +102,9 @@ const SelectDropdown: React.FC<SelectDropdownProps> = ({
onClick={() => !disabled && setIsOpen(!isOpen)}
disabled={disabled}
onKeyDown={handleKeyDown}
className={`igny8-select-styled h-9 w-full appearance-none rounded-lg border border-gray-300 bg-transparent px-3 py-2 pr-10 text-sm shadow-theme-xs focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:focus:border-brand-800 ${
className={`igny8-select-styled w-full appearance-none rounded-lg border border-gray-300 bg-transparent px-3 pr-10 shadow-theme-xs focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:focus:border-brand-800 ${
className.includes('text-base') ? 'h-11 py-2.5 text-base' : 'h-9 py-2 text-sm'
} ${
isPlaceholder
? "text-gray-400 dark:text-gray-400"
: "text-gray-800 dark:text-white/90"
@@ -140,7 +142,9 @@ const SelectDropdown: React.FC<SelectDropdownProps> = ({
// Pass the normalized optionValue to ensure consistency
handleSelect(optionValue);
}}
className={`w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2 ${
className={`w-full text-left px-3 py-2 transition-colors flex items-center gap-2 ${
className.includes('text-base') ? 'text-base' : 'text-sm'
} ${
isSelected
? "bg-brand-500 text-white"
: "text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"

View File

@@ -1,15 +1,13 @@
/**
* WorkflowGuide Component
* Inline welcome/guide screen for new users
* Shows complete workflow explainer with visual flow maps and progress tracking
* Shows site creation form with industry and sector selection
*/
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { Card } from '../ui/card';
import Button from '../ui/button/Button';
import Badge from '../ui/badge/Badge';
import { ProgressBar } from '../ui/progress';
import Checkbox from '../form/input/Checkbox';
import {
CloseIcon,
ArrowRightIcon,
@@ -24,33 +22,32 @@ import {
} from '../../icons';
import { useOnboardingStore } from '../../store/onboardingStore';
import { useSiteStore } from '../../store/siteStore';
import { fetchSites, fetchKeywords, fetchClusters, fetchContent } from '../../services/api';
import { fetchSites, fetchIndustries, Industry, Sector, createSite, selectSectorsForSite, setActiveSite } from '../../services/api';
import { useAuthStore } from '../../store/authStore';
import SelectDropdown from '../form/SelectDropdown';
import { useToast } from '../ui/toast/ToastContainer';
import Alert from '../ui/alert/Alert';
interface WorkflowProgress {
hasSite: boolean;
keywordsCount: number;
clustersCount: number;
contentCount: number;
publishedCount: number;
completionPercentage: number;
interface WorkflowGuideProps {
onSiteAdded?: () => void;
}
export default function WorkflowGuide() {
export default function WorkflowGuide({ onSiteAdded }: WorkflowGuideProps) {
const navigate = useNavigate();
const toast = useToast();
const { isGuideVisible, dismissGuide, loadFromBackend } = useOnboardingStore();
const { activeSite } = useSiteStore();
const { activeSite, loadActiveSite } = useSiteStore();
const { isAuthenticated } = useAuthStore();
const [progress, setProgress] = useState<WorkflowProgress>({
hasSite: false,
keywordsCount: 0,
clustersCount: 0,
contentCount: 0,
publishedCount: 0,
completionPercentage: 0,
});
const [loadingProgress, setLoadingProgress] = useState(true);
const [dontShowAgain, setDontShowAgain] = useState(false);
// Industry and Sector selection state
const [industries, setIndustries] = useState<Industry[]>([]);
const [selectedIndustry, setSelectedIndustry] = useState<Industry | null>(null);
const [selectedSectors, setSelectedSectors] = useState<string[]>([]);
const [loadingIndustries, setLoadingIndustries] = useState(false);
const [isWordPressExpanded, setIsWordPressExpanded] = useState(false);
const [isCreatingSite, setIsCreatingSite] = useState(false);
const [siteName, setSiteName] = useState('');
const [websiteAddress, setWebsiteAddress] = useState('');
// Load dismissal state from backend on mount
useEffect(() => {
@@ -61,56 +58,137 @@ export default function WorkflowGuide() {
}
}, [isAuthenticated, loadFromBackend]);
// Load progress data
// Load industries on mount
useEffect(() => {
if (!isAuthenticated || !isGuideVisible) return;
const loadProgress = async () => {
const loadIndustries = async () => {
try {
setLoadingProgress(true);
const [sitesRes, keywordsRes, clustersRes, contentRes] = await Promise.all([
fetchSites().catch(() => ({ results: [], count: 0 })),
fetchKeywords({ page_size: 1 }).catch(() => ({ count: 0 })),
fetchClusters({ page_size: 1 }).catch(() => ({ count: 0 })),
fetchContent({ page_size: 1 }).catch(() => ({ count: 0 })),
]);
const sitesCount = sitesRes.results?.filter((s: any) => s.is_active).length || 0;
const keywordsCount = keywordsRes.count || 0;
const clustersCount = clustersRes.count || 0;
const contentCount = contentRes.count || 0;
const publishedCount = 0; // TODO: Add published content count when API is available
// Calculate completion percentage
// Milestones: Site (20%), Keywords (20%), Clusters (20%), Content (20%), Published (20%)
let completion = 0;
if (sitesCount > 0) completion += 20;
if (keywordsCount > 0) completion += 20;
if (clustersCount > 0) completion += 20;
if (contentCount > 0) completion += 20;
if (publishedCount > 0) completion += 20;
setProgress({
hasSite: sitesCount > 0,
keywordsCount,
clustersCount,
contentCount,
publishedCount,
completionPercentage: completion,
});
setLoadingIndustries(true);
const response = await fetchIndustries();
setIndustries(response.industries || []);
} catch (error) {
console.error('Failed to load progress:', error);
console.error('Failed to load industries:', error);
} finally {
setLoadingProgress(false);
setLoadingIndustries(false);
}
};
loadProgress();
loadIndustries();
}, [isAuthenticated, isGuideVisible]);
if (!isGuideVisible) return null;
// Get available sectors for selected industry
const availableSectors = useMemo(() => {
if (!selectedIndustry) return [];
return selectedIndustry.sectors || [];
}, [selectedIndustry]);
const { hasSite, keywordsCount, clustersCount, contentCount, publishedCount, completionPercentage } = progress;
// Handle industry selection
const handleIndustrySelect = (industry: Industry) => {
setSelectedIndustry(industry);
setSelectedSectors([]); // Reset sectors when industry changes
};
// Handle sector toggle
const handleSectorToggle = (sectorSlug: string) => {
setSelectedSectors(prev =>
prev.includes(sectorSlug)
? prev.filter(s => s !== sectorSlug)
: [...prev, sectorSlug]
);
};
// Handle WordPress card click - expand to show industry/sector selection
const handleWordPressCardClick = () => {
setIsWordPressExpanded(!isWordPressExpanded);
if (!isWordPressExpanded && industries.length === 0) {
// Load industries if not already loaded
const loadIndustries = async () => {
try {
setLoadingIndustries(true);
const response = await fetchIndustries();
setIndustries(response.industries || []);
} catch (error) {
console.error('Failed to load industries:', error);
} finally {
setLoadingIndustries(false);
}
};
loadIndustries();
}
};
// Handle Add Site button click
const handleAddSite = async () => {
if (!siteName || !siteName.trim()) {
toast.error('Please enter a site name');
return;
}
if (!selectedIndustry) {
toast.error('Please select an industry first');
return;
}
if (selectedSectors.length === 0) {
toast.error('Please select at least one sector');
return;
}
if (selectedSectors.length > 5) {
toast.error('Maximum 5 sectors allowed per site');
return;
}
try {
setIsCreatingSite(true);
// Create site with user-provided name and domain
const newSite = await createSite({
name: siteName.trim(),
domain: websiteAddress.trim() || undefined,
is_active: true,
hosting_type: 'wordpress',
});
// Set industry for the site (if API supports it during creation, otherwise update)
// For now, we'll set it via selectSectorsForSite which also sets industry
// Select sectors for the site (this also sets the industry)
await selectSectorsForSite(
newSite.id,
selectedIndustry.slug,
selectedSectors
);
// Set as active site if it's the first site
const sitesRes = await fetchSites();
if (sitesRes.results.length === 1) {
await setActiveSite(newSite.id);
}
const siteNameTrimmed = siteName.trim();
toast.success(`Site "${siteNameTrimmed}" created successfully with ${selectedSectors.length} sector${selectedSectors.length !== 1 ? 's' : ''}!`);
// Reload sites in store
await loadActiveSite();
// Call callback if provided (before navigation)
if (onSiteAdded) {
onSiteAdded();
}
// Dismiss guide and redirect to site settings with integrations tab
await dismissGuide();
navigate(`/sites/${newSite.id}/settings?tab=integrations`);
} catch (error: any) {
toast.error(`Failed to create site: ${error.message}`);
} finally {
setIsCreatingSite(false);
}
};
if (!isGuideVisible) return null;
return (
<div className="mb-8">
@@ -142,413 +220,215 @@ export default function WorkflowGuide() {
</Button>
</div>
{/* Main Workflow Options */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
{/* Build New Site */}
<Card className="p-6 border-2 border-blue-200 dark:border-blue-800 hover:border-blue-400 dark:hover:border-blue-600 transition-colors">
<div className="flex items-start gap-4 mb-4">
{/* Site Integration Option */}
<div className="mb-6">
<Card className="p-6 border-2 border-blue-200 dark:border-blue-800">
<div className="flex items-start gap-4 mb-6">
<div className="size-12 rounded-xl bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center text-white flex-shrink-0">
<GridIcon className="h-6 w-6" />
</div>
<div className="flex-1">
<h3 className="text-lg font-bold text-gray-900 dark:text-white mb-1">
Build New Site
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
Create a new website from scratch with IGNY8
</p>
</div>
</div>
<div className="space-y-3">
<button
onClick={async () => {
navigate('/sites/builder?type=wordpress');
await dismissGuide();
}}
className="w-full flex items-center justify-between p-4 rounded-lg border-2 border-gray-200 dark:border-gray-700 hover:border-blue-400 dark:hover:border-blue-600 bg-white dark:bg-gray-800 transition-colors group"
>
<div className="flex items-center gap-3">
<PlugInIcon className="w-5 h-5 text-blue-600 dark:text-blue-400" />
<div className="text-left">
<div className="font-semibold text-gray-900 dark:text-white">
WordPress Self-Hosted Site
</div>
<div className="text-xs text-gray-500 dark:text-gray-400">
Build and sync to your WordPress installation
</div>
</div>
</div>
<ArrowRightIcon className="w-5 h-5 text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors" />
</button>
<button
onClick={async () => {
navigate('/sites/builder?type=igny8');
await dismissGuide();
}}
className="w-full flex items-center justify-between p-4 rounded-lg border-2 border-gray-200 dark:border-gray-700 hover:border-blue-400 dark:hover:border-blue-600 bg-white dark:bg-gray-800 transition-colors group"
>
<div className="flex items-center gap-3">
<GridIcon className="w-5 h-5 text-blue-600 dark:text-blue-400" />
<div className="text-left">
<div className="font-semibold text-gray-900 dark:text-white">
IGNY8-Powered IGNY8-Hosted Site
</div>
<div className="text-xs text-gray-500 dark:text-gray-400">
Fully managed site hosted by IGNY8
</div>
</div>
</div>
<ArrowRightIcon className="w-5 h-5 text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors" />
</button>
</div>
</Card>
{/* Integrate Existing Site */}
<Card className="p-6 border-2 border-green-200 dark:border-green-800 hover:border-green-400 dark:hover:border-green-600 transition-colors">
<div className="flex items-start gap-4 mb-4">
<div className="size-12 rounded-xl bg-gradient-to-br from-green-500 to-green-600 flex items-center justify-center text-white flex-shrink-0">
<PlugInIcon className="h-6 w-6" />
</div>
<div className="flex-1">
<h3 className="text-lg font-bold text-gray-900 dark:text-white mb-1">
Integrate Existing Site
Integrate New Site or Existing Site
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
Connect your existing website to IGNY8
Connect your WordPress site to IGNY8
</p>
</div>
</div>
<div className="space-y-3">
{/* Site Type Selection - 2 columns for future IGNY8 option */}
<div className="grid grid-cols-2 gap-4 mb-6">
{/* WordPress Site Card - Clickable to expand */}
<button
onClick={async () => {
navigate('/sites?action=integrate&platform=wordpress');
await dismissGuide();
}}
className="w-full flex items-center justify-between p-4 rounded-lg border-2 border-gray-200 dark:border-gray-700 hover:border-green-400 dark:hover:border-green-600 bg-white dark:bg-gray-800 transition-colors group"
onClick={handleWordPressCardClick}
className={`flex items-center justify-between p-4 rounded-lg border-2 transition-all duration-200 group ${
isWordPressExpanded
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
: 'border-gray-200 dark:border-gray-700 hover:border-blue-400 dark:hover:border-blue-600 bg-white dark:bg-gray-800'
}`}
>
<div className="flex items-center gap-3">
<PlugInIcon className="w-5 h-5 text-green-600 dark:text-green-400" />
<PlugInIcon className={`w-5 h-5 ${isWordPressExpanded ? 'text-blue-600 dark:text-blue-400' : 'text-blue-600 dark:text-blue-400'}`} />
<div className="text-left">
<div className="font-semibold text-gray-900 dark:text-white">
Integrate WordPress/Shopify
WordPress Site
</div>
<div className="text-xs text-gray-500 dark:text-gray-400">
Sync content with your existing site
Integrate new or existing WordPress site
</div>
</div>
</div>
<ArrowRightIcon className="w-5 h-5 text-gray-400 group-hover:text-green-600 dark:group-hover:text-green-400 transition-colors" />
<ArrowRightIcon className={`w-5 h-5 transition-transform ${isWordPressExpanded ? 'transform rotate-90' : ''} ${
isWordPressExpanded
? 'text-blue-600 dark:text-blue-400'
: 'text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400'
}`} />
</button>
<button
onClick={async () => {
navigate('/sites?action=integrate&platform=custom');
await dismissGuide();
}}
className="w-full flex items-center justify-between p-4 rounded-lg border-2 border-gray-200 dark:border-gray-700 hover:border-green-400 dark:hover:border-green-600 bg-white dark:bg-gray-800 transition-colors group"
>
<div className="flex items-center gap-3">
<FileIcon className="w-5 h-5 text-green-600 dark:text-green-400" />
<div className="text-left">
<div className="font-semibold text-gray-900 dark:text-white">
Custom Site
</div>
<div className="text-xs text-gray-500 dark:text-gray-400">
Connect any custom website
</div>
{/* Placeholder for future IGNY8 option */}
<div className="flex items-center justify-center p-4 rounded-lg border-2 border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50 opacity-50">
<div className="text-center">
<div className="font-semibold text-gray-500 dark:text-gray-400 text-sm">
IGNY8 Hosted
</div>
<div className="text-xs text-gray-400 dark:text-gray-500">
Coming Soon
</div>
</div>
<ArrowRightIcon className="w-5 h-5 text-gray-400 group-hover:text-green-600 dark:group-hover:text-green-400 transition-colors" />
</button>
</div>
</div>
{/* Expanded Site Form - 3 columns: Name, Website Address, Industry */}
{isWordPressExpanded && (
<div className="space-y-4 pt-4 border-t border-gray-200 dark:border-gray-700">
{/* Site Name, Website Address, and Industry - 3 columns */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* Site Name */}
<div>
<label className="block text-base font-semibold text-gray-900 dark:text-white mb-2">
Site Name
</label>
<input
type="text"
value={siteName}
onChange={(e) => setSiteName(e.target.value)}
placeholder="Enter site name"
className="w-full px-4 py-2.5 border-2 border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-base focus:border-blue-500 focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-800"
/>
</div>
{/* Website Address */}
<div>
<label className="block text-base font-semibold text-gray-900 dark:text-white mb-2">
Website Address
</label>
<input
type="text"
value={websiteAddress}
onChange={(e) => setWebsiteAddress(e.target.value)}
placeholder="https://example.com"
className="w-full px-4 py-2.5 border-2 border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-base focus:border-blue-500 focus:ring-2 focus:ring-blue-200 dark:focus:ring-blue-800"
/>
</div>
{/* Industry Selection - Using SelectDropdown */}
<div>
<label className="block text-base font-semibold text-gray-900 dark:text-white mb-2">
Select Industry
</label>
{loadingIndustries ? (
<div className="text-base text-gray-500 py-2.5">Loading industries...</div>
) : (
<SelectDropdown
options={industries.map(industry => ({
value: industry.slug,
label: industry.name,
}))}
placeholder="-- Select Industry --"
value={selectedIndustry?.slug || ''}
onChange={(value) => {
const industry = industries.find(i => i.slug === value);
if (industry) {
handleIndustrySelect(industry);
}
}}
className="text-base [&_.igny8-select-styled]:text-base [&_.igny8-select-styled]:py-2.5 [&_button]:text-base"
/>
)}
</div>
</div>
{/* Sector Selection - Cards like IndustriesSectorsKeywords page */}
{selectedIndustry && availableSectors.length > 0 && (
<div>
<label className="block text-base font-semibold text-gray-900 dark:text-white mb-2">
Select Sectors
</label>
<div className="grid grid-cols-5 gap-3">
{availableSectors.map((sector) => (
<Card
key={sector.slug}
className={`p-4 hover:shadow-lg transition-all duration-200 border-2 cursor-pointer ${
selectedSectors.includes(sector.slug)
? 'border-[var(--color-primary)] bg-blue-50 dark:bg-blue-900/20'
: 'border-gray-200 dark:border-gray-700'
}`}
onClick={() => handleSectorToggle(sector.slug)}
>
<div className="flex justify-between items-start mb-2">
<h3 className="text-base font-semibold text-gray-900 dark:text-white">
{sector.name}
</h3>
{selectedSectors.includes(sector.slug) && (
<CheckCircleIcon className="w-5 h-5 text-[var(--color-primary)] flex-shrink-0" />
)}
</div>
{sector.description && (
<p className="text-xs text-gray-600 dark:text-gray-400">
{sector.description}
</p>
)}
</Card>
))}
</div>
{selectedSectors.length > 0 && (
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
{selectedSectors.length} sector{selectedSectors.length !== 1 ? 's' : ''} selected
</p>
)}
</div>
)}
{/* Validation Notification Card */}
{(!siteName || !siteName.trim() || !selectedIndustry || selectedSectors.length === 0) && (
<div className="pt-4 flex justify-center">
<div className="w-full max-w-md">
<div className="[&_h4]:hidden [&_p]:text-lg [&_p]:font-semibold">
<Alert
variant="info"
title=""
message={
!siteName || !siteName.trim()
? 'Please enter a site name'
: !selectedIndustry
? 'Please select an industry'
: 'Please select at least one sector'
}
/>
</div>
</div>
</div>
)}
{/* Add Site Button */}
<div className="pt-4 flex justify-center">
<Button
onClick={handleAddSite}
disabled={!siteName || !siteName.trim() || !selectedIndustry || selectedSectors.length === 0 || isCreatingSite}
variant="solid"
tone="brand"
size="lg"
className={`text-lg font-semibold px-8 py-4 h-auto transition-all duration-200 ${
!siteName || !siteName.trim() || !selectedIndustry || selectedSectors.length === 0 || isCreatingSite
? ''
: 'hover:shadow-lg hover:scale-[1.01] active:scale-[0.99]'
}`}
>
{isCreatingSite ? (
<>Creating Site...</>
) : (
<>Add Site</>
)}
</Button>
</div>
</div>
)}
</Card>
</div>
{/* Workflow Steps */}
<div className="mb-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Your Content Creation Workflow
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{[
{ icon: <ListIcon />, label: 'Discover Keywords', gradient: 'from-blue-500 to-blue-600', path: '/planner/keywords' },
{ icon: <GroupIcon />, label: 'Cluster Keywords', gradient: 'from-purple-500 to-purple-600', path: '/planner/clusters' },
{ icon: <BoltIcon />, label: 'Generate Ideas', gradient: 'from-orange-500 to-orange-600', path: '/planner/ideas' },
{ icon: <FileTextIcon />, label: 'Create Content', gradient: 'from-green-500 to-green-600', path: '/writer/content' },
].map((step, index) => (
<button
key={index}
onClick={() => navigate(step.path)}
className="flex items-center gap-3 p-4 rounded-lg border-2 border-gray-200 dark:border-gray-700 hover:border-[var(--color-primary)] dark:hover:border-[var(--color-primary)] bg-white dark:bg-gray-800 transition-colors group"
>
<div className={`size-10 rounded-lg bg-gradient-to-br ${step.gradient} flex items-center justify-center text-white flex-shrink-0`}>
{React.cloneElement(step.icon as React.ReactElement, { className: 'w-5 h-5' })}
</div>
<div className="text-left">
<div className="font-semibold text-sm text-gray-900 dark:text-white group-hover:text-[var(--color-primary)]">
{step.label}
</div>
</div>
</button>
))}
</div>
</div>
{/* Progress Tracking */}
<div className="mb-6">
<div className="flex items-center justify-between mb-3">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
Your Progress
</h3>
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
{completionPercentage}% Complete
</span>
</div>
<ProgressBar
value={completionPercentage}
className="mb-4"
/>
{/* Milestones */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
{[
{
label: 'Site Created',
completed: hasSite,
count: hasSite ? 1 : 0,
path: '/sites',
icon: <GridIcon className="w-4 h-4" />
},
{
label: 'Keywords',
completed: keywordsCount > 0,
count: keywordsCount,
path: '/planner/keywords',
icon: <ListIcon className="w-4 h-4" />
},
{
label: 'Clusters',
completed: clustersCount > 0,
count: clustersCount,
path: '/planner/clusters',
icon: <GroupIcon className="w-4 h-4" />
},
{
label: 'Content',
completed: contentCount > 0,
count: contentCount,
path: '/writer/content',
icon: <FileTextIcon className="w-4 h-4" />
},
{
label: 'Published',
completed: publishedCount > 0,
count: publishedCount,
path: '/writer/published',
icon: <CheckCircleIcon className="w-4 h-4" />
},
].map((milestone, index) => (
<button
key={index}
onClick={() => {
navigate(milestone.path);
dismissGuide();
}}
className={`flex flex-col items-center gap-2 p-3 rounded-lg border-2 transition-colors ${
milestone.completed
? 'border-green-300 dark:border-green-700 bg-green-50 dark:bg-green-950/20'
: 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:border-orange-400 dark:hover:border-orange-600'
}`}
>
<div className={`flex items-center justify-center w-8 h-8 rounded-lg ${
milestone.completed
? 'bg-green-500 text-white'
: 'bg-gray-200 dark:bg-gray-700 text-gray-500 dark:text-gray-400'
}`}>
{milestone.completed ? (
<CheckCircleIcon className="w-5 h-5" />
) : (
milestone.icon
)}
</div>
<div className="text-center">
<div className={`text-xs font-semibold ${
milestone.completed
? 'text-green-700 dark:text-green-300'
: 'text-gray-600 dark:text-gray-400'
}`}>
{milestone.label}
</div>
<div className={`text-xs ${
milestone.completed
? 'text-green-600 dark:text-green-400'
: 'text-gray-500 dark:text-gray-500'
}`}>
{milestone.count}
</div>
</div>
</button>
))}
</div>
</div>
{/* Contextual CTA based on progress */}
{completionPercentage === 0 && (
<div className="flex items-center gap-2 p-4 rounded-lg bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-800 mb-6">
<BoltIcon className="w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0" />
<div className="flex-1">
<div className="font-semibold text-sm text-blue-900 dark:text-blue-100">
Get started by creating your first site
</div>
<div className="text-xs text-blue-700 dark:text-blue-300 mt-1">
Choose one of the options above to begin
</div>
</div>
</div>
)}
{hasSite && keywordsCount === 0 && (
<div className="flex items-center gap-2 p-4 rounded-lg bg-purple-50 dark:bg-purple-950/20 border border-purple-200 dark:border-purple-800 mb-6">
<CheckCircleIcon className="w-5 h-5 text-purple-600 dark:text-purple-400 flex-shrink-0" />
<div className="flex-1">
<div className="font-semibold text-sm text-purple-900 dark:text-purple-100">
Great! You've created your first site
</div>
<div className="text-xs text-purple-700 dark:text-purple-300 mt-1">
Now discover keywords to start your content planning
</div>
</div>
<Button
variant="outline"
size="sm"
onClick={async () => {
navigate('/planner/keywords');
await dismissGuide();
}}
>
Discover Keywords
<ArrowRightIcon className="w-4 h-4 ml-2" />
</Button>
</div>
)}
{keywordsCount > 0 && clustersCount === 0 && (
<div className="flex items-center gap-2 p-4 rounded-lg bg-indigo-50 dark:bg-indigo-950/20 border border-indigo-200 dark:border-indigo-800 mb-6">
<CheckCircleIcon className="w-5 h-5 text-indigo-600 dark:text-indigo-400 flex-shrink-0" />
<div className="flex-1">
<div className="font-semibold text-sm text-indigo-900 dark:text-indigo-100">
{keywordsCount} keywords added!
</div>
<div className="text-xs text-indigo-700 dark:text-indigo-300 mt-1">
Group them into clusters to organize your content strategy
</div>
</div>
<Button
variant="outline"
size="sm"
onClick={async () => {
navigate('/planner/clusters');
await dismissGuide();
}}
>
Create Clusters
<ArrowRightIcon className="w-4 h-4 ml-2" />
</Button>
</div>
)}
{clustersCount > 0 && contentCount === 0 && (
<div className="flex items-center gap-2 p-4 rounded-lg bg-orange-50 dark:bg-orange-950/20 border border-orange-200 dark:border-orange-800 mb-6">
<CheckCircleIcon className="w-5 h-5 text-orange-600 dark:text-orange-400 flex-shrink-0" />
<div className="flex-1">
<div className="font-semibold text-sm text-orange-900 dark:text-orange-100">
{clustersCount} clusters ready!
</div>
<div className="text-xs text-orange-700 dark:text-orange-300 mt-1">
Generate content ideas and start writing
</div>
</div>
<Button
variant="outline"
size="sm"
onClick={async () => {
navigate('/writer/tasks');
await dismissGuide();
}}
>
Create Content
<ArrowRightIcon className="w-4 h-4 ml-2" />
</Button>
</div>
)}
{contentCount > 0 && publishedCount === 0 && (
<div className="flex items-center gap-2 p-4 rounded-lg bg-green-50 dark:bg-green-950/20 border border-green-200 dark:border-green-800 mb-6">
<CheckCircleIcon className="w-5 h-5 text-green-600 dark:text-green-400 flex-shrink-0" />
<div className="flex-1">
<div className="font-semibold text-sm text-green-900 dark:text-green-100">
{contentCount} content pieces created!
</div>
<div className="text-xs text-green-700 dark:text-green-300 mt-1">
Review and publish your content to go live
</div>
</div>
<Button
variant="outline"
size="sm"
onClick={async () => {
navigate('/writer/content');
await dismissGuide();
}}
>
Review Content
<ArrowRightIcon className="w-4 h-4 ml-2" />
</Button>
</div>
)}
{/* Footer Actions */}
<div className="pt-4 border-t border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Checkbox
checked={dontShowAgain}
onChange={setDontShowAgain}
label="Don't show this again"
id="dont-show-guide"
/>
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={async () => {
if (dontShowAgain) {
await dismissGuide();
}
navigate('/sites');
}}
>
View All Sites
</Button>
<Button
variant="primary"
size="sm"
onClick={async () => {
if (dontShowAgain) {
await dismissGuide();
}
navigate('/planner/keywords');
}}
>
Start Planning
<ArrowRightIcon className="w-4 h-4 ml-2" />
</Button>
</div>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400">
You can always access this guide from the orange "Show Guide" button in the header
</p>
</div>
</Card>
</div>
);