diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 906544c1..7bbda04a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -58,6 +58,9 @@ const Usage = lazy(() => import("./pages/Billing/Usage")); const SeedKeywords = lazy(() => import("./pages/Reference/SeedKeywords")); const ReferenceIndustries = lazy(() => import("./pages/Reference/Industries")); +// Setup Pages - Lazy loaded +const IndustriesSectorsKeywords = lazy(() => import("./pages/Setup/IndustriesSectorsKeywords")); + // Other Pages - Lazy loaded const AutomationDashboard = lazy(() => import("./pages/Automation/Dashboard")); const AutomationRules = lazy(() => import("./pages/Automation/Rules")); @@ -331,6 +334,13 @@ export default function App() { } /> + {/* Setup Pages */} + + + + } /> + {/* Automation Module - Redirect dashboard to rules */} } /> ; +} + +interface UserPreferences { + selectedIndustry?: string; + selectedSectors?: string[]; + selectedKeywords?: number[]; +} + +// Format volume with k for thousands and m for millions +const formatVolume = (volume: number): string => { + if (volume >= 1000000) { + return `${(volume / 1000000).toFixed(1)}m`; + } else if (volume >= 1000) { + return `${(volume / 1000).toFixed(1)}k`; + } + return volume.toString(); +}; + +export default function IndustriesSectorsKeywords() { + const toast = useToast(); + const { pageSize } = usePageSizeStore(); + + // Data state + const [industries, setIndustries] = useState([]); + const [selectedIndustry, setSelectedIndustry] = useState(null); + const [selectedSectors, setSelectedSectors] = useState([]); + const [seedKeywords, setSeedKeywords] = useState([]); + const [loading, setLoading] = useState(true); + const [keywordsLoading, setKeywordsLoading] = useState(false); + const [saving, setSaving] = useState(false); + + // Keywords table state + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [totalCount, setTotalCount] = useState(0); + const [searchTerm, setSearchTerm] = useState(''); + const [intentFilter, setIntentFilter] = useState(''); + const [difficultyFilter, setDifficultyFilter] = useState(''); + const [selectedKeywordIds, setSelectedKeywordIds] = useState([]); + + // Load industries on mount + useEffect(() => { + loadIndustries(); + loadUserPreferences(); + }, []); + + // Load user preferences from account settings + const loadUserPreferences = async () => { + try { + const setting = await fetchAccountSetting('user_preferences'); + const preferences = setting.config as UserPreferences | undefined; + if (preferences) { + if (preferences.selectedIndustry) { + // Find and select the industry + const industry = industries.find(i => i.slug === preferences.selectedIndustry); + if (industry) { + setSelectedIndustry(industry); + if (preferences.selectedSectors) { + setSelectedSectors(preferences.selectedSectors); + } + } + } + } + } catch (error) { + // If preferences don't exist yet, that's fine + console.log('No user preferences found'); + } + }; + + // Load industries + const loadIndustries = async () => { + try { + setLoading(true); + const response = await fetchIndustries(); + const industriesList = response.industries || []; + + // Fetch keywords to calculate counts + let allKeywords: SeedKeyword[] = []; + try { + const keywordsResponse = await fetchSeedKeywords({ + page_size: 1000, + }); + allKeywords = keywordsResponse.results || []; + } catch (error) { + console.warn('Failed to fetch keywords for counts:', error); + } + + // Process each industry with its keywords data + const industriesWithData = industriesList.map((industry) => { + const industryKeywords = allKeywords.filter( + (kw: SeedKeyword) => kw.industry_name === industry.name + ); + + const totalVolume = industryKeywords.reduce( + (sum, kw) => sum + (kw.volume || 0), + 0 + ); + + return { + ...industry, + keywordsCount: industryKeywords.length, + totalVolume, + }; + }); + + setIndustries(industriesWithData.filter(i => i.keywordsCount > 0)); + } catch (error: any) { + toast.error(`Failed to load industries: ${error.message}`); + } finally { + setLoading(false); + } + }; + + // Load sectors for selected industry + const loadSectorsForIndustry = useCallback(async () => { + if (!selectedIndustry) { + return; + } + + try { + // Fetch sectors from the industry's related data + // The industry object should have sectors in the response + const response = await fetchIndustries(); + const industryData = response.industries?.find(i => i.slug === selectedIndustry.slug); + if (industryData?.sectors) { + // Sectors are already in the industry data + } + } catch (error: any) { + console.error('Error loading sectors:', error); + } + }, [selectedIndustry]); + + // Load keywords when industry/sectors change + useEffect(() => { + if (selectedIndustry) { + loadKeywords(); + } else { + setSeedKeywords([]); + setTotalCount(0); + } + }, [selectedIndustry, selectedSectors, currentPage, searchTerm, intentFilter, difficultyFilter]); + + // Load seed keywords + const loadKeywords = async () => { + if (!selectedIndustry) { + return; + } + + setKeywordsLoading(true); + try { + const filters: any = { + industry_name: selectedIndustry.name, + page: currentPage, + page_size: pageSize, + }; + + if (selectedSectors.length > 0) { + // Filter by selected sectors if any + // Note: API might need sector_name filter + } + + if (searchTerm) { + filters.search = searchTerm; + } + + if (intentFilter) { + filters.intent = intentFilter; + } + + if (difficultyFilter) { + const difficultyNum = parseInt(difficultyFilter); + const label = getDifficultyLabelFromNumber(difficultyNum); + if (label !== null) { + const range = getDifficultyRange(label); + if (range) { + filters.difficulty_min = range.min; + filters.difficulty_max = range.max; + } + } + } + + const data = await fetchSeedKeywords(filters); + setSeedKeywords(data.results || []); + setTotalCount(data.count || 0); + setTotalPages(Math.ceil((data.count || 0) / pageSize)); + } catch (error: any) { + toast.error(`Failed to load keywords: ${error.message}`); + } finally { + setKeywordsLoading(false); + } + }; + + // Handle industry selection + const handleIndustrySelect = (industry: Industry) => { + setSelectedIndustry(industry); + setSelectedSectors([]); // Reset sectors when industry changes + setCurrentPage(1); + }; + + // Handle sector toggle + const handleSectorToggle = (sectorSlug: string) => { + setSelectedSectors(prev => + prev.includes(sectorSlug) + ? prev.filter(s => s !== sectorSlug) + : [...prev, sectorSlug] + ); + setCurrentPage(1); + }; + + // Save preferences to account + const handleSavePreferences = async () => { + if (!selectedIndustry) { + toast.error('Please select an industry first'); + return; + } + + setSaving(true); + try { + const preferences: UserPreferences = { + selectedIndustry: selectedIndustry.slug, + selectedSectors: selectedSectors, + selectedKeywords: selectedKeywordIds.map(id => parseInt(id)), + }; + + // Try to update existing setting, or create if it doesn't exist + try { + await updateAccountSetting('user_preferences', { + config: preferences, + is_active: true, + }); + } catch (error: any) { + // If setting doesn't exist, create it + if (error.status === 404) { + await createAccountSetting({ + key: 'user_preferences', + config: preferences, + is_active: true, + }); + } else { + throw error; + } + } + + toast.success('Preferences saved successfully! These will be used when creating new sites.'); + } catch (error: any) { + toast.error(`Failed to save preferences: ${error.message}`); + } finally { + setSaving(false); + } + }; + + // Get available sectors for selected industry + const availableSectors = useMemo(() => { + if (!selectedIndustry) return []; + // Get sectors from industry data or fetch separately + // For now, we'll use the industry's sectors if available + return selectedIndustry.sectors || []; + }, [selectedIndustry]); + + // Keywords table columns + const keywordColumns = useMemo(() => [ + { + key: 'keyword', + label: 'Keyword', + sortable: true, + render: (row: SeedKeyword) => ( +
+ {row.keyword} +
+ ), + }, + { + key: 'industry_name', + label: 'Industry', + sortable: true, + render: (row: SeedKeyword) => ( + {row.industry_name} + ), + }, + { + key: 'sector_name', + label: 'Sector', + sortable: true, + render: (row: SeedKeyword) => ( + {row.sector_name} + ), + }, + { + key: 'volume', + label: 'Volume', + sortable: true, + render: (row: SeedKeyword) => ( + + {row.volume ? formatVolume(row.volume) : '-'} + + ), + }, + { + key: 'difficulty', + label: 'Difficulty', + sortable: true, + render: (row: SeedKeyword) => { + const difficulty = row.difficulty || 0; + const label = difficulty < 30 ? 'Easy' : difficulty < 70 ? 'Medium' : 'Hard'; + const color = difficulty < 30 ? 'success' : difficulty < 70 ? 'warning' : 'error'; + return {label}; + }, + }, + { + key: 'intent', + label: 'Intent', + sortable: true, + render: (row: SeedKeyword) => ( + {row.intent || 'N/A'} + ), + }, + ], []); + + return ( + <> + + , color: 'blue' }} + hideSiteSector={true} + /> + +
+ + {(activeTab, setActiveTab) => ( + <> + + setActiveTab('industries')}> + Industries + + setActiveTab('sectors')} disabled={!selectedIndustry}> + Sectors {selectedIndustry && `(${selectedSectors.length})`} + + setActiveTab('keywords')} disabled={!selectedIndustry}> + Keywords {selectedIndustry && `(${totalCount})`} + + + + +
+
+

+ Select Your Industry +

+

+ Choose the industry that best matches your business. This will be used as a default when creating new sites. +

+
+ + {loading ? ( +
+
Loading industries...
+
+ ) : ( +
+ {industries.map((industry) => ( + handleIndustrySelect(industry)} + > +
+

+ {industry.name} +

+ {selectedIndustry?.slug === industry.slug && ( + + )} +
+ + {industry.description && ( +

+ {industry.description} +

+ )} + +
+
+ {industry.sectors?.length || 0} + sectors +
+
+ {industry.keywordsCount || 0} + keywords +
+
+ + {industry.totalVolume > 0 && ( +
+
+ + Total volume: {formatVolume(industry.totalVolume)} +
+
+ )} +
+ ))} +
+ )} +
+
+ + +
+
+

+ Select Sectors for {selectedIndustry?.name} +

+

+ Choose one or more sectors within your selected industry. These will be used as defaults when creating new sites. +

+
+ + {!selectedIndustry ? ( +
+ Please select an industry first +
+ ) : availableSectors.length === 0 ? ( +
+ No sectors available for this industry +
+ ) : ( +
+ {availableSectors.map((sector) => ( + handleSectorToggle(sector.slug)} + > +
+

+ {sector.name} +

+ {selectedSectors.includes(sector.slug) && ( + + )} +
+ {sector.description && ( +

+ {sector.description} +

+ )} +
+ ))} +
+ )} +
+
+ + +
+
+

+ Keyword Opportunities for {selectedIndustry?.name} +

+

+ Browse and select keywords that match your selected industry and sectors. These will be available when creating new sites. +

+
+ + {!selectedIndustry ? ( +
+ Please select an industry first +
+ ) : ( + { + if (key === 'search') setSearchTerm(value as string); + else if (key === 'intent') setIntentFilter(value as string); + else if (key === 'difficulty') setDifficultyFilter(value as string); + setCurrentPage(1); + }} + pagination={{ + currentPage, + totalPages, + totalCount, + onPageChange: setCurrentPage, + }} + selection={{ + selectedIds: selectedKeywordIds, + onSelectionChange: setSelectedKeywordIds, + }} + /> + )} +
+
+ + )} +
+ + {/* Save Button */} + {selectedIndustry && ( +
+ +
+ )} +
+ + ); +} + diff --git a/frontend/src/pages/Sites/Dashboard.tsx b/frontend/src/pages/Sites/Dashboard.tsx index e7b59304..0dbf4cd2 100644 --- a/frontend/src/pages/Sites/Dashboard.tsx +++ b/frontend/src/pages/Sites/Dashboard.tsx @@ -5,22 +5,25 @@ */ import React, { useState, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; -import { - EyeIcon, - FileTextIcon, - PlugIcon, - TrendingUpIcon, - CalendarIcon, - GlobeIcon, - RefreshCwIcon, - RocketIcon -} from 'lucide-react'; import PageMeta from '../../components/common/PageMeta'; +import PageHeader from '../../components/common/PageHeader'; import { Card } from '../../components/ui/card'; import Button from '../../components/ui/button/Button'; +import EnhancedMetricCard from '../../components/dashboard/EnhancedMetricCard'; import { useToast } from '../../components/ui/toast/ToastContainer'; import { fetchAPI, fetchSiteBlueprints } from '../../services/api'; import SiteProgressWidget from '../../components/sites/SiteProgressWidget'; +import { + EyeIcon, + FileIcon, + PlugInIcon, + ArrowUpIcon, + CalendarIcon, + GridIcon, + BoltIcon, + PageIcon, + ArrowRightIcon +} from '../../icons'; interface Site { id: number; @@ -136,70 +139,68 @@ export default function SiteDashboard() { const statCards = [ { - label: 'Total Pages', + title: 'Total Pages', value: stats?.total_pages || 0, - icon: , - color: 'blue', - link: `/sites/${siteId}/pages`, + icon: , + accentColor: 'blue' as const, + href: `/sites/${siteId}/pages`, }, { - label: 'Published Pages', + title: 'Published Pages', value: stats?.published_pages || 0, - icon: , - color: 'green', - link: `/sites/${siteId}/pages?status=published`, + icon: , + accentColor: 'success' as const, + href: `/sites/${siteId}/pages?status=published`, }, { - label: 'Draft Pages', + title: 'Draft Pages', value: stats?.draft_pages || 0, - icon: , - color: 'amber', - link: `/sites/${siteId}/pages?status=draft`, + icon: , + accentColor: 'orange' as const, + href: `/sites/${siteId}/pages?status=draft`, }, { - label: 'Integrations', + title: 'Integrations', value: stats?.integrations_count || 0, - icon: , - color: 'purple', - link: `/sites/${siteId}/settings?tab=integrations`, + icon: , + accentColor: 'purple' as const, + href: `/sites/${siteId}/settings?tab=integrations`, }, { - label: 'Deployments', + title: 'Deployments', value: stats?.deployments_count || 0, - icon: , - color: 'teal', - link: `/sites/${siteId}/preview`, + icon: , + accentColor: 'blue' as const, + href: `/sites/${siteId}/preview`, }, { - label: 'Total Content', + title: 'Total Content', value: stats?.total_content || 0, - icon: , - color: 'indigo', - link: `/sites/${siteId}/content`, + icon: , + accentColor: 'purple' as const, + href: `/sites/${siteId}/content`, }, ]; return (
+ , color: 'blue' }} + /> - {/* Header */} -
-
-

- {site.name} -

-

- {site.slug} • {site.site_type} • {site.hosting_type} + {/* Site Info */} +

+

+ {site.slug} • {site.site_type} • {site.hosting_type} +

+ {site.domain && ( +

+ {site.domain}

- {site.domain && ( -

- - {site.domain} -

- )} -
-
+ )} +
)} - {/* Stats Grid */} + {/* Stats Grid - Using EnhancedMetricCard */}
{statCards.map((stat, index) => ( - stat.link && navigate(stat.link)} - > -
-
-

- {stat.label} -

-

- {stat.value} -

-
-
- {stat.icon} -
-
-
+ title={stat.title} + value={stat.value} + icon={stat.icon} + accentColor={stat.accentColor} + href={stat.href} + /> ))}
- {/* Quick Actions */} - + + {/* Quick Actions - Matching Planner Dashboard pattern */} +

Quick Actions

-
- -
+
+

Manage Pages

+

View and edit pages

+
+ + + + -
+
+

Manage Content

+

View and edit content

+
+ + + + - -
+
+

Integrations

+

Manage connections

+
+ + + + -
+
+

Sync Dashboard

+

View sync status

+
+ + + + +
+ +
+
+

Deploy Site

+

Deploy to production

+
+ +
- +
{/* Recent Activity */} @@ -319,7 +331,9 @@ export default function SiteDashboard() {
{stats?.last_deployment ? (
- +
+ +

Last Deployment diff --git a/frontend/src/pages/Sites/List.tsx b/frontend/src/pages/Sites/List.tsx index 2f982060..3db5916f 100644 --- a/frontend/src/pages/Sites/List.tsx +++ b/frontend/src/pages/Sites/List.tsx @@ -5,8 +5,8 @@ */ import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; -import { PlusIcon, EditIcon, SettingsIcon, EyeIcon, TrashIcon, FilterIcon, SearchIcon, PlugIcon, FileTextIcon, MoreVerticalIcon, Building2Icon } from 'lucide-react'; import PageMeta from '../../components/common/PageMeta'; +import PageHeader from '../../components/common/PageHeader'; import { Card } from '../../components/ui/card'; import Button from '../../components/ui/button/Button'; import SelectDropdown from '../../components/form/SelectDropdown'; @@ -17,6 +17,17 @@ import Badge from '../../components/ui/badge/Badge'; import FormModal, { FormField } from '../../components/common/FormModal'; import Alert from '../../components/ui/alert/Alert'; import Switch from '../../components/form/switch/Switch'; +import { + PlusIcon, + PencilIcon, + EyeIcon, + TrashBinIcon, + GridIcon, + PlugInIcon, + FileIcon, + MoreDotIcon, + PageIcon +} from '../../icons'; import { fetchSites, createSite, @@ -503,17 +514,16 @@ export default function SiteList() { return (

+ , color: 'blue' }} + />
-
-

- Sites Management -

-

- Manage your sites, configure industries, and select sectors. Multiple sites can be active simultaneously. -

-
+

+ Manage your sites, configure industries, and select sectors. Multiple sites can be active simultaneously. +

@@ -737,7 +747,7 @@ export default function SiteList() { className="shadow-theme-xs inline-flex h-9 items-center justify-center rounded-lg border border-gray-300 text-gray-700 dark:border-gray-700 dark:text-gray-400 px-3" title="Configure Sectors" > - + Sectors
diff --git a/refactor-plan/FINAL_REFACTOR_TASKS.md b/refactor-plan/FINAL_REFACTOR_TASKS.md index a340055c..bdc693cb 100644 --- a/refactor-plan/FINAL_REFACTOR_TASKS.md +++ b/refactor-plan/FINAL_REFACTOR_TASKS.md @@ -298,9 +298,25 @@ 3. Add validation checks in components ### Phase 4: Design Consistency - Core Sites Pages (HIGH Priority) -1. Refactor Sites Dashboard -2. Refactor Sites List -3. Refactor Sites Builder pages + +**Design System Requirements:** +- **Colors**: Use CSS variables `var(--color-primary)`, `var(--color-success)`, `var(--color-warning)`, `var(--color-purple)` and their `-dark` variants +- **Gradients**: Use `from-[var(--color-primary)] to-[var(--color-primary-dark)]` pattern for icon backgrounds (matching Planner/Writer dashboards) +- **Icons**: Import from `../../icons` (not lucide-react), use same icon patterns as Planner/Writer/Thinker/Automation/Dashboard +- **Components**: Use standard `Button`, `Card`, `Badge`, `PageHeader`, `EnhancedMetricCard` from design system +- **Layout**: Use same spacing (`p-6`, `gap-4`, `rounded-xl`), border-radius, shadow patterns as Dashboard/Planner/Writer pages +- **Buttons**: Use standard Button component with `variant="primary"`, `variant="outline"` etc. +- **Cards**: Use standard Card component or EnhancedMetricCard for metrics (matching Dashboard/Planner patterns) +- **Badges**: Use standard Badge component with color variants matching design system +- **Stat Cards**: Use gradient icon backgrounds like Planner Dashboard: `bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)]` +- **Hover States**: Use `hover:border-[var(--color-primary)]` pattern for interactive cards + +**Completed:** +1. ✅ Refactor Sites Dashboard - Replaced lucide-react icons, using EnhancedMetricCard, standard colors/gradients, PageHeader component, matching Planner Dashboard patterns +2. ✅ Refactor Sites List - Replaced lucide-react icons, using standard Button/Card/Badge components, matching Dashboard styling, gradient icon backgrounds + +**Remaining:** +3. Refactor Sites Builder pages - Apply same design system patterns ### Phase 5: Design Consistency - Remaining Sites Pages (MEDIUM Priority) 1. Refactor Sites Settings