refactor phase 7-8
This commit is contained in:
@@ -117,12 +117,19 @@ export default function Home() {
|
||||
const toast = useToast();
|
||||
const { activeSite } = useSiteStore();
|
||||
const { activeSector } = useSectorStore();
|
||||
const { isGuideDismissed, showGuide } = useOnboardingStore();
|
||||
const { isGuideDismissed, showGuide, loadFromBackend } = useOnboardingStore();
|
||||
|
||||
const [insights, setInsights] = useState<AppInsights | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [lastUpdated, setLastUpdated] = useState<Date>(new Date());
|
||||
|
||||
// Load guide state from backend on mount
|
||||
useEffect(() => {
|
||||
loadFromBackend().catch(() => {
|
||||
// Silently fail - local state will be used
|
||||
});
|
||||
}, [loadFromBackend]);
|
||||
|
||||
// Show guide on first visit if not dismissed
|
||||
useEffect(() => {
|
||||
if (!isGuideDismissed) {
|
||||
|
||||
@@ -2,12 +2,14 @@ import { useState, useEffect, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import PageMeta from '../../components/common/PageMeta';
|
||||
import PageHeader from '../../components/common/PageHeader';
|
||||
import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs';
|
||||
import ModuleMetricsFooter, { MetricItem, ProgressMetric } from '../../components/dashboard/ModuleMetricsFooter';
|
||||
import { linkerApi } from '../../api/linker.api';
|
||||
import { fetchContent, Content as ContentType } from '../../services/api';
|
||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||
import { SourceBadge, ContentSource } from '../../components/content/SourceBadge';
|
||||
import { LinkResults } from '../../components/linker/LinkResults';
|
||||
import { PlugInIcon } from '../../icons';
|
||||
import { PlugInIcon, CheckCircleIcon, FileIcon } from '../../icons';
|
||||
import { useSectorStore } from '../../store/sectorStore';
|
||||
import { usePageSizeStore } from '../../store/pageSizeStore';
|
||||
|
||||
@@ -103,6 +105,9 @@ export default function LinkerContentList() {
|
||||
title="Link Content"
|
||||
description="Add internal links to your content"
|
||||
/>
|
||||
<ModuleNavigationTabs tabs={[
|
||||
{ label: 'Content', path: '/linker/content', icon: <FileIcon /> },
|
||||
]} />
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center py-12">
|
||||
@@ -240,6 +245,41 @@ export default function LinkerContentList() {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Module Metrics Footer */}
|
||||
<ModuleMetricsFooter
|
||||
metrics={[
|
||||
{
|
||||
title: 'Total Content',
|
||||
value: totalCount.toLocaleString(),
|
||||
subtitle: `${content.filter(c => (c.internal_links?.length || 0) > 0).length} with links`,
|
||||
icon: <FileIcon className="w-5 h-5" />,
|
||||
accentColor: 'blue',
|
||||
href: '/linker/content',
|
||||
},
|
||||
{
|
||||
title: 'Links Added',
|
||||
value: content.reduce((sum, c) => sum + (c.internal_links?.length || 0), 0).toLocaleString(),
|
||||
subtitle: `${Object.keys(linkResults).length} processed`,
|
||||
icon: <PlugInIcon className="w-5 h-5" />,
|
||||
accentColor: 'purple',
|
||||
},
|
||||
{
|
||||
title: 'Avg Links/Content',
|
||||
value: content.length > 0
|
||||
? (content.reduce((sum, c) => sum + (c.internal_links?.length || 0), 0) / content.length).toFixed(1)
|
||||
: '0',
|
||||
subtitle: `${content.filter(c => c.linker_version && c.linker_version > 0).length} optimized`,
|
||||
icon: <CheckCircleIcon className="w-5 h-5" />,
|
||||
accentColor: 'green',
|
||||
},
|
||||
]}
|
||||
progress={{
|
||||
label: 'Content Linking Progress',
|
||||
value: totalCount > 0 ? Math.round((content.filter(c => (c.internal_links?.length || 0) > 0).length / totalCount) * 100) : 0,
|
||||
color: 'primary',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useState, useEffect, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import PageMeta from '../../components/common/PageMeta';
|
||||
import PageHeader from '../../components/common/PageHeader';
|
||||
import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs';
|
||||
import ModuleMetricsFooter, { MetricItem, ProgressMetric } from '../../components/dashboard/ModuleMetricsFooter';
|
||||
import { optimizerApi, EntryPoint } from '../../api/optimizer.api';
|
||||
import { fetchContent, Content as ContentType } from '../../services/api';
|
||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||
@@ -9,7 +11,7 @@ import { SourceBadge, ContentSource } from '../../components/content/SourceBadge
|
||||
import { SyncStatusBadge, SyncStatus } from '../../components/content/SyncStatusBadge';
|
||||
import { ContentFilter, FilterState } from '../../components/content/ContentFilter';
|
||||
import { OptimizationScores } from '../../components/optimizer/OptimizationScores';
|
||||
import { BoltIcon, CheckCircleIcon } from '../../icons';
|
||||
import { BoltIcon, CheckCircleIcon, FileIcon } from '../../icons';
|
||||
import { useSectorStore } from '../../store/sectorStore';
|
||||
import { usePageSizeStore } from '../../store/pageSizeStore';
|
||||
|
||||
@@ -146,15 +148,18 @@ export default function OptimizerContentSelector() {
|
||||
<PageMeta title="Optimize Content" description="Select and optimize content for SEO and engagement" />
|
||||
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
title="Optimize Content"
|
||||
lastUpdated={new Date()}
|
||||
badge={{
|
||||
icon: <BoltIcon />,
|
||||
color: 'orange',
|
||||
}}
|
||||
/>
|
||||
<ModuleNavigationTabs tabs={[
|
||||
{ label: 'Content', path: '/optimizer/content', icon: <FileIcon /> },
|
||||
]} />
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<PageHeader
|
||||
title="Optimize Content"
|
||||
lastUpdated={new Date()}
|
||||
badge={{
|
||||
icon: <BoltIcon />,
|
||||
color: 'orange',
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center gap-4">
|
||||
<select
|
||||
value={entryPoint}
|
||||
@@ -326,6 +331,47 @@ export default function OptimizerContentSelector() {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Module Metrics Footer */}
|
||||
<ModuleMetricsFooter
|
||||
metrics={[
|
||||
{
|
||||
title: 'Total Content',
|
||||
value: totalCount.toLocaleString(),
|
||||
subtitle: `${filteredContent.length} filtered`,
|
||||
icon: <FileIcon className="w-5 h-5" />,
|
||||
accentColor: 'blue',
|
||||
href: '/optimizer/content',
|
||||
},
|
||||
{
|
||||
title: 'Optimized',
|
||||
value: content.filter(c => c.optimizer_version && c.optimizer_version > 0).length.toLocaleString(),
|
||||
subtitle: `${processing.length} processing`,
|
||||
icon: <BoltIcon className="w-5 h-5" />,
|
||||
accentColor: 'orange',
|
||||
},
|
||||
{
|
||||
title: 'Avg Score',
|
||||
value: content.length > 0 && content.some(c => c.optimization_scores?.overall_score)
|
||||
? (content
|
||||
.filter(c => c.optimization_scores?.overall_score)
|
||||
.reduce((sum, c) => sum + (c.optimization_scores?.overall_score || 0), 0) /
|
||||
content.filter(c => c.optimization_scores?.overall_score).length
|
||||
).toFixed(1)
|
||||
: '-',
|
||||
subtitle: `${content.filter(c => c.optimization_scores?.overall_score && c.optimization_scores.overall_score >= 80).length} high score`,
|
||||
icon: <CheckCircleIcon className="w-5 h-5" />,
|
||||
accentColor: 'green',
|
||||
},
|
||||
]}
|
||||
progress={{
|
||||
label: 'Content Optimization Progress',
|
||||
value: totalCount > 0
|
||||
? Math.round((content.filter(c => c.optimizer_version && c.optimizer_version > 0).length / totalCount) * 100)
|
||||
: 0,
|
||||
color: 'warning',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -28,6 +28,7 @@ import { useSectorStore } from '../../store/sectorStore';
|
||||
import { usePageSizeStore } from '../../store/pageSizeStore';
|
||||
import PageHeader from '../../components/common/PageHeader';
|
||||
import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs';
|
||||
import ModuleMetricsFooter, { MetricItem, ProgressMetric } from '../../components/dashboard/ModuleMetricsFooter';
|
||||
import { getDifficultyLabelFromNumber, getDifficultyRange } from '../../utils/difficulty';
|
||||
import FormModal from '../../components/common/FormModal';
|
||||
import ProgressModal from '../../components/common/ProgressModal';
|
||||
@@ -881,6 +882,40 @@ export default function Keywords() {
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Module Metrics Footer */}
|
||||
<ModuleMetricsFooter
|
||||
metrics={[
|
||||
{
|
||||
title: 'Total Keywords',
|
||||
value: totalCount.toLocaleString(),
|
||||
subtitle: `${clusters.length} clusters`,
|
||||
icon: <ListIcon className="w-5 h-5" />,
|
||||
accentColor: 'blue',
|
||||
href: '/planner/keywords',
|
||||
},
|
||||
{
|
||||
title: 'Clustered',
|
||||
value: keywords.filter(k => k.cluster_id).length.toLocaleString(),
|
||||
subtitle: `${Math.round((keywords.filter(k => k.cluster_id).length / Math.max(totalCount, 1)) * 100)}% coverage`,
|
||||
icon: <GroupIcon className="w-5 h-5" />,
|
||||
accentColor: 'purple',
|
||||
href: '/planner/clusters',
|
||||
},
|
||||
{
|
||||
title: 'Active',
|
||||
value: keywords.filter(k => k.status === 'active').length.toLocaleString(),
|
||||
subtitle: `${keywords.filter(k => k.status === 'pending').length} pending`,
|
||||
icon: <BoltIcon className="w-5 h-5" />,
|
||||
accentColor: 'green',
|
||||
},
|
||||
]}
|
||||
progress={{
|
||||
label: 'Keyword Clustering Progress',
|
||||
value: totalCount > 0 ? Math.round((keywords.filter(k => k.cluster_id).length / totalCount) * 100) : 0,
|
||||
color: 'primary',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Create/Edit Modal */}
|
||||
<FormModal
|
||||
key={`keyword-form-${isEditMode ? editingKeyword?.id : 'new'}-${formData.seed_keyword_id}-${formData.status}`}
|
||||
|
||||
@@ -205,6 +205,38 @@ function BusinessDetailsStepStage1({
|
||||
metadata?: SiteBuilderMetadata;
|
||||
selectedSectors?: Array<{ id: number; name: string }>;
|
||||
}) {
|
||||
const [userPreferences, setUserPreferences] = useState<{
|
||||
selectedIndustry?: string;
|
||||
selectedSectors?: string[];
|
||||
} | null>(null);
|
||||
const [loadingPreferences, setLoadingPreferences] = useState(true);
|
||||
|
||||
// Load user preferences from account settings
|
||||
useEffect(() => {
|
||||
const loadPreferences = async () => {
|
||||
try {
|
||||
const { fetchAccountSetting } = await import('../../../../services/api');
|
||||
const setting = await fetchAccountSetting('user_preferences');
|
||||
const preferences = setting.config as { selectedIndustry?: string; selectedSectors?: string[] } | undefined;
|
||||
if (preferences) {
|
||||
setUserPreferences(preferences);
|
||||
// Pre-populate industry if available and not already set
|
||||
if (preferences.selectedIndustry && !data.industry) {
|
||||
onChange('industry', preferences.selectedIndustry);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
// 404 means preferences don't exist yet - that's fine
|
||||
if (error.status !== 404) {
|
||||
console.warn('Failed to load user preferences:', error);
|
||||
}
|
||||
} finally {
|
||||
setLoadingPreferences(false);
|
||||
}
|
||||
};
|
||||
loadPreferences();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Card variant="surface" padding="lg" className="space-y-6">
|
||||
<div>
|
||||
@@ -216,7 +248,12 @@ function BusinessDetailsStepStage1({
|
||||
Business details
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 max-w-2xl">
|
||||
These inputs help the AI understand what we’re building. You can refine them later in the builder or site settings.
|
||||
These inputs help the AI understand what we're building. You can refine them later in the builder or site settings.
|
||||
{userPreferences?.selectedIndustry && (
|
||||
<span className="block mt-1 text-xs text-green-600 dark:text-green-400">
|
||||
✓ Using your pre-selected industry and sectors from setup
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -268,9 +305,8 @@ function BusinessDetailsStepStage1({
|
||||
<Input
|
||||
value={data.industry}
|
||||
onChange={(e) => onChange('industry', e.target.value)}
|
||||
placeholder="Supply chain automation"
|
||||
placeholder={userPreferences?.selectedIndustry || "Supply chain automation"}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-3 rounded-2xl border border-gray-100 bg-white p-4 shadow-theme-sm dark:border-white/5 dark:bg-white/[0.02]">
|
||||
<label className="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
Hosting preference
|
||||
|
||||
@@ -72,6 +72,10 @@ export default function SiteList() {
|
||||
const [selectedIndustry, setSelectedIndustry] = useState<string>('');
|
||||
const [selectedSectors, setSelectedSectors] = useState<string[]>([]);
|
||||
const [isSelectingSectors, setIsSelectingSectors] = useState(false);
|
||||
const [userPreferences, setUserPreferences] = useState<{
|
||||
selectedIndustry?: string;
|
||||
selectedSectors?: string[];
|
||||
} | null>(null);
|
||||
|
||||
// Form state for site creation/editing
|
||||
const [formData, setFormData] = useState({
|
||||
@@ -91,8 +95,25 @@ export default function SiteList() {
|
||||
useEffect(() => {
|
||||
loadSites();
|
||||
loadIndustries();
|
||||
loadUserPreferences();
|
||||
}, []);
|
||||
|
||||
const loadUserPreferences = async () => {
|
||||
try {
|
||||
const { fetchAccountSetting } = await import('../../services/api');
|
||||
const setting = await fetchAccountSetting('user_preferences');
|
||||
const preferences = setting.config as { selectedIndustry?: string; selectedSectors?: string[] } | undefined;
|
||||
if (preferences) {
|
||||
setUserPreferences(preferences);
|
||||
}
|
||||
} catch (error: any) {
|
||||
// 404 means preferences don't exist yet - that's fine
|
||||
if (error.status !== 404) {
|
||||
console.warn('Failed to load user preferences:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
applyFilters();
|
||||
}, [sites, searchTerm, siteTypeFilter, hostingTypeFilter, statusFilter, integrationFilter]);
|
||||
@@ -132,7 +153,26 @@ export default function SiteList() {
|
||||
const loadIndustries = async () => {
|
||||
try {
|
||||
const response = await fetchIndustries();
|
||||
setIndustries(response.industries || []);
|
||||
let allIndustries = response.industries || [];
|
||||
|
||||
// Filter to show only user's pre-selected industries/sectors from account preferences
|
||||
try {
|
||||
const { fetchAccountSetting } = await import('../../services/api');
|
||||
const setting = await fetchAccountSetting('user_preferences');
|
||||
const preferences = setting.config as { selectedIndustry?: string; selectedSectors?: string[] } | undefined;
|
||||
|
||||
if (preferences?.selectedIndustry) {
|
||||
// Filter industries to only show the user's pre-selected industry
|
||||
allIndustries = allIndustries.filter(i => i.slug === preferences.selectedIndustry);
|
||||
}
|
||||
} catch (error: any) {
|
||||
// 404 means preferences don't exist yet - show all industries
|
||||
if (error.status !== 404) {
|
||||
console.warn('Failed to load user preferences for filtering:', error);
|
||||
}
|
||||
}
|
||||
|
||||
setIndustries(allIndustries);
|
||||
} catch (error: any) {
|
||||
console.error('Failed to load industries:', error);
|
||||
}
|
||||
@@ -387,7 +427,14 @@ export default function SiteList() {
|
||||
const getIndustrySectors = () => {
|
||||
if (!selectedIndustry) return [];
|
||||
const industry = industries.find(i => i.slug === selectedIndustry);
|
||||
return industry?.sectors || [];
|
||||
let sectors = industry?.sectors || [];
|
||||
|
||||
// Filter to show only user's pre-selected sectors from account preferences
|
||||
if (userPreferences?.selectedSectors && userPreferences.selectedSectors.length > 0) {
|
||||
sectors = sectors.filter(s => userPreferences.selectedSectors!.includes(s.slug));
|
||||
}
|
||||
|
||||
return sectors;
|
||||
};
|
||||
|
||||
const clearFilters = () => {
|
||||
|
||||
@@ -22,6 +22,7 @@ import ProgressModal from '../../components/common/ProgressModal';
|
||||
import { useProgressModal } from '../../hooks/useProgressModal';
|
||||
import PageHeader from '../../components/common/PageHeader';
|
||||
import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs';
|
||||
import ModuleMetricsFooter, { MetricItem, ProgressMetric } from '../../components/dashboard/ModuleMetricsFooter';
|
||||
|
||||
export default function Content() {
|
||||
const toast = useToast();
|
||||
@@ -259,6 +260,39 @@ export default function Content() {
|
||||
getItemDisplayName={(row: ContentType) => row.meta_title || row.title || `Content #${row.id}`}
|
||||
/>
|
||||
|
||||
{/* Module Metrics Footer */}
|
||||
<ModuleMetricsFooter
|
||||
metrics={[
|
||||
{
|
||||
title: 'Total Content',
|
||||
value: totalCount.toLocaleString(),
|
||||
subtitle: `${content.filter(c => c.status === 'published').length} published`,
|
||||
icon: <FileIcon className="w-5 h-5" />,
|
||||
accentColor: 'green',
|
||||
href: '/writer/content',
|
||||
},
|
||||
{
|
||||
title: 'Draft',
|
||||
value: content.filter(c => c.status === 'draft').length.toLocaleString(),
|
||||
subtitle: `${content.filter(c => c.status === 'review').length} in review`,
|
||||
icon: <TaskIcon className="w-5 h-5" />,
|
||||
accentColor: 'blue',
|
||||
},
|
||||
{
|
||||
title: 'Synced',
|
||||
value: content.filter(c => c.sync_status === 'synced').length.toLocaleString(),
|
||||
subtitle: `${content.filter(c => c.sync_status === 'pending').length} pending`,
|
||||
icon: <CheckCircleIcon className="w-5 h-5" />,
|
||||
accentColor: 'purple',
|
||||
},
|
||||
]}
|
||||
progress={{
|
||||
label: 'Content Publishing Progress',
|
||||
value: totalCount > 0 ? Math.round((content.filter(c => c.status === 'published').length / totalCount) * 100) : 0,
|
||||
color: 'success',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Progress Modal for AI Functions */}
|
||||
<ProgressModal
|
||||
isOpen={progressModal.isOpen}
|
||||
|
||||
Reference in New Issue
Block a user