437 lines
18 KiB
TypeScript
437 lines
18 KiB
TypeScript
/**
|
|
* WorkflowGuide Component
|
|
* Inline welcome/guide screen for new users
|
|
* Shows site creation form with industry and sector selection
|
|
*/
|
|
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 {
|
|
CloseIcon,
|
|
ArrowRightIcon,
|
|
GridIcon,
|
|
PlugInIcon,
|
|
FileIcon,
|
|
CheckCircleIcon,
|
|
BoltIcon,
|
|
ListIcon,
|
|
GroupIcon,
|
|
FileTextIcon,
|
|
} from '../../icons';
|
|
import { useOnboardingStore } from '../../store/onboardingStore';
|
|
import { useSiteStore } from '../../store/siteStore';
|
|
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 WorkflowGuideProps {
|
|
onSiteAdded?: () => void;
|
|
}
|
|
|
|
export default function WorkflowGuide({ onSiteAdded }: WorkflowGuideProps) {
|
|
const navigate = useNavigate();
|
|
const toast = useToast();
|
|
const { isGuideVisible, dismissGuide, loadFromBackend } = useOnboardingStore();
|
|
const { activeSite, loadActiveSite } = useSiteStore();
|
|
const { isAuthenticated } = useAuthStore();
|
|
|
|
// 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(() => {
|
|
if (isAuthenticated) {
|
|
loadFromBackend().catch(() => {
|
|
// Silently fail - local state will be used
|
|
});
|
|
}
|
|
}, [isAuthenticated, loadFromBackend]);
|
|
|
|
// Load industries on mount
|
|
useEffect(() => {
|
|
if (!isAuthenticated || !isGuideVisible) return;
|
|
|
|
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();
|
|
}, [isAuthenticated, isGuideVisible]);
|
|
|
|
// Get available sectors for selected industry
|
|
const availableSectors = useMemo(() => {
|
|
if (!selectedIndustry) return [];
|
|
return selectedIndustry.sectors || [];
|
|
}, [selectedIndustry]);
|
|
|
|
// 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">
|
|
<Card className="rounded-2xl border-2 border-orange-200 bg-gradient-to-br from-orange-50 to-white dark:from-orange-950/20 dark:to-gray-900 dark:border-orange-800 p-6 md:p-8">
|
|
{/* Header */}
|
|
<div className="flex items-start justify-between mb-6">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<div className="size-12 rounded-xl bg-gradient-to-br from-orange-500 to-orange-600 flex items-center justify-center text-white shadow-lg">
|
|
<BoltIcon className="h-6 w-6" />
|
|
</div>
|
|
<div>
|
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
|
|
Welcome to IGNY8
|
|
</h2>
|
|
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
|
Your complete AI-powered content creation workflow
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={async () => await dismissGuide()}
|
|
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
|
>
|
|
<CloseIcon className="w-5 h-5" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 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">
|
|
<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 New Site or Existing Site
|
|
</h3>
|
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
Connect your WordPress site to IGNY8
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 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={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 ${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">
|
|
WordPress Site
|
|
</div>
|
|
<div className="text-xs text-gray-500 dark:text-gray-400">
|
|
Integrate new or existing WordPress site
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<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>
|
|
|
|
{/* 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>
|
|
</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>
|
|
|
|
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|