refactor phase 7-8
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
/**
|
||||
* WorkflowGuide Component
|
||||
* Inline welcome/guide screen for new users
|
||||
* Shows complete workflow explainer with visual flow maps
|
||||
* Shows complete workflow explainer with visual flow maps and progress tracking
|
||||
*/
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } 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,
|
||||
@@ -22,15 +24,93 @@ import {
|
||||
} from '../../icons';
|
||||
import { useOnboardingStore } from '../../store/onboardingStore';
|
||||
import { useSiteStore } from '../../store/siteStore';
|
||||
import { fetchSites, fetchKeywords, fetchClusters, fetchContent } from '../../services/api';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
|
||||
interface WorkflowProgress {
|
||||
hasSite: boolean;
|
||||
keywordsCount: number;
|
||||
clustersCount: number;
|
||||
contentCount: number;
|
||||
publishedCount: number;
|
||||
completionPercentage: number;
|
||||
}
|
||||
|
||||
export default function WorkflowGuide() {
|
||||
const navigate = useNavigate();
|
||||
const { isGuideVisible, dismissGuide } = useOnboardingStore();
|
||||
const { isGuideVisible, dismissGuide, loadFromBackend } = useOnboardingStore();
|
||||
const { activeSite } = 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);
|
||||
|
||||
// Load dismissal state from backend on mount
|
||||
useEffect(() => {
|
||||
if (isAuthenticated) {
|
||||
loadFromBackend().catch(() => {
|
||||
// Silently fail - local state will be used
|
||||
});
|
||||
}
|
||||
}, [isAuthenticated, loadFromBackend]);
|
||||
|
||||
// Load progress data
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated || !isGuideVisible) return;
|
||||
|
||||
const loadProgress = 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,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to load progress:', error);
|
||||
} finally {
|
||||
setLoadingProgress(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadProgress();
|
||||
}, [isAuthenticated, isGuideVisible]);
|
||||
|
||||
if (!isGuideVisible) return null;
|
||||
|
||||
const hasSite = !!activeSite;
|
||||
const { hasSite, keywordsCount, clustersCount, contentCount, publishedCount, completionPercentage } = progress;
|
||||
|
||||
return (
|
||||
<div className="mb-8">
|
||||
@@ -55,7 +135,7 @@ export default function WorkflowGuide() {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={dismissGuide}
|
||||
onClick={async () => await dismissGuide()}
|
||||
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
>
|
||||
<CloseIcon className="w-5 h-5" />
|
||||
@@ -81,9 +161,9 @@ export default function WorkflowGuide() {
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
onClick={async () => {
|
||||
navigate('/sites/builder?type=wordpress');
|
||||
dismissGuide();
|
||||
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"
|
||||
>
|
||||
@@ -101,9 +181,9 @@ export default function WorkflowGuide() {
|
||||
<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={() => {
|
||||
onClick={async () => {
|
||||
navigate('/sites/builder?type=igny8');
|
||||
dismissGuide();
|
||||
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"
|
||||
>
|
||||
@@ -140,9 +220,9 @@ export default function WorkflowGuide() {
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
onClick={async () => {
|
||||
navigate('/sites?action=integrate&platform=wordpress');
|
||||
dismissGuide();
|
||||
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"
|
||||
>
|
||||
@@ -160,9 +240,9 @@ export default function WorkflowGuide() {
|
||||
<ArrowRightIcon className="w-5 h-5 text-gray-400 group-hover:text-green-600 dark:group-hover:text-green-400 transition-colors" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
onClick={async () => {
|
||||
navigate('/sites?action=integrate&platform=custom');
|
||||
dismissGuide();
|
||||
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"
|
||||
>
|
||||
@@ -213,60 +293,261 @@ export default function WorkflowGuide() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress Indicator (if user has started) */}
|
||||
{hasSite && (
|
||||
<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">
|
||||
<CheckCircleIcon className="w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0" />
|
||||
{/* 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">
|
||||
Great! You've created your first site
|
||||
Get started by creating your first site
|
||||
</div>
|
||||
<div className="text-xs text-blue-700 dark:text-blue-300 mt-1">
|
||||
Continue with keyword research and content planning
|
||||
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={() => {
|
||||
onClick={async () => {
|
||||
navigate('/planner/keywords');
|
||||
dismissGuide();
|
||||
await dismissGuide();
|
||||
}}
|
||||
>
|
||||
Get Started
|
||||
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="flex items-center justify-between pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
You can always access this guide from the header
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
navigate('/sites');
|
||||
dismissGuide();
|
||||
}}
|
||||
>
|
||||
View All Sites
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
navigate('/planner/keywords');
|
||||
dismissGuide();
|
||||
}}
|
||||
>
|
||||
Start Planning
|
||||
<ArrowRightIcon className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user