refactor phase 7-8

This commit is contained in:
alorig
2025-11-20 22:40:18 +05:00
parent 45dc0d1fa2
commit 3e142afc7a
11 changed files with 695 additions and 74 deletions

View File

@@ -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>