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

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

View File

@@ -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>
</>
);

View File

@@ -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>
</>
);

View File

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

View File

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

View File

@@ -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 = () => {

View File

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