refactor-frontend-sites pages
This commit is contained in:
@@ -58,6 +58,9 @@ const Usage = lazy(() => import("./pages/Billing/Usage"));
|
|||||||
const SeedKeywords = lazy(() => import("./pages/Reference/SeedKeywords"));
|
const SeedKeywords = lazy(() => import("./pages/Reference/SeedKeywords"));
|
||||||
const ReferenceIndustries = lazy(() => import("./pages/Reference/Industries"));
|
const ReferenceIndustries = lazy(() => import("./pages/Reference/Industries"));
|
||||||
|
|
||||||
|
// Setup Pages - Lazy loaded
|
||||||
|
const IndustriesSectorsKeywords = lazy(() => import("./pages/Setup/IndustriesSectorsKeywords"));
|
||||||
|
|
||||||
// Other Pages - Lazy loaded
|
// Other Pages - Lazy loaded
|
||||||
const AutomationDashboard = lazy(() => import("./pages/Automation/Dashboard"));
|
const AutomationDashboard = lazy(() => import("./pages/Automation/Dashboard"));
|
||||||
const AutomationRules = lazy(() => import("./pages/Automation/Rules"));
|
const AutomationRules = lazy(() => import("./pages/Automation/Rules"));
|
||||||
@@ -331,6 +334,13 @@ export default function App() {
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
} />
|
} />
|
||||||
|
|
||||||
|
{/* Setup Pages */}
|
||||||
|
<Route path="/setup/industries-sectors-keywords" element={
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<IndustriesSectorsKeywords />
|
||||||
|
</Suspense>
|
||||||
|
} />
|
||||||
|
|
||||||
{/* Automation Module - Redirect dashboard to rules */}
|
{/* Automation Module - Redirect dashboard to rules */}
|
||||||
<Route path="/automation" element={<Navigate to="/automation/rules" replace />} />
|
<Route path="/automation" element={<Navigate to="/automation/rules" replace />} />
|
||||||
<Route path="/automation/rules" element={
|
<Route path="/automation/rules" element={
|
||||||
|
|||||||
597
frontend/src/pages/Setup/IndustriesSectorsKeywords.tsx
Normal file
597
frontend/src/pages/Setup/IndustriesSectorsKeywords.tsx
Normal file
@@ -0,0 +1,597 @@
|
|||||||
|
/**
|
||||||
|
* Industries, Sectors & Keywords Setup Page
|
||||||
|
* Merged page combining:
|
||||||
|
* - Industry selection
|
||||||
|
* - Sector selection (from selected industry)
|
||||||
|
* - Keyword opportunities (filtered by selected industry/sectors)
|
||||||
|
*
|
||||||
|
* Saves selections to user account for use in site creation
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
|
import PageMeta from '../../components/common/PageMeta';
|
||||||
|
import PageHeader from '../../components/common/PageHeader';
|
||||||
|
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||||
|
import {
|
||||||
|
fetchIndustries,
|
||||||
|
Industry,
|
||||||
|
fetchSeedKeywords,
|
||||||
|
SeedKeyword,
|
||||||
|
fetchAccountSetting,
|
||||||
|
createAccountSetting,
|
||||||
|
updateAccountSetting
|
||||||
|
} from '../../services/api';
|
||||||
|
import { Card } from '../../components/ui/card';
|
||||||
|
import Badge from '../../components/ui/badge/Badge';
|
||||||
|
import Button from '../../components/ui/button/Button';
|
||||||
|
import { PieChartIcon, CheckCircleIcon, BoltIcon } from '../../icons';
|
||||||
|
import { Tooltip } from '../../components/ui/tooltip/Tooltip';
|
||||||
|
import { Tabs, TabList, Tab, TabPanel } from '../../components/ui/tabs/Tabs';
|
||||||
|
import TablePageTemplate from '../../templates/TablePageTemplate';
|
||||||
|
import { usePageSizeStore } from '../../store/pageSizeStore';
|
||||||
|
import { getDifficultyLabelFromNumber, getDifficultyRange } from '../../utils/difficulty';
|
||||||
|
|
||||||
|
interface IndustryWithData extends Industry {
|
||||||
|
keywordsCount: number;
|
||||||
|
totalVolume: number;
|
||||||
|
sectors?: Array<{ slug: string; name: string; description?: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<IndustryWithData[]>([]);
|
||||||
|
const [selectedIndustry, setSelectedIndustry] = useState<Industry | null>(null);
|
||||||
|
const [selectedSectors, setSelectedSectors] = useState<string[]>([]);
|
||||||
|
const [seedKeywords, setSeedKeywords] = useState<SeedKeyword[]>([]);
|
||||||
|
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<string[]>([]);
|
||||||
|
|
||||||
|
// 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) => (
|
||||||
|
<div className="font-medium text-gray-900 dark:text-white">
|
||||||
|
{row.keyword}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'industry_name',
|
||||||
|
label: 'Industry',
|
||||||
|
sortable: true,
|
||||||
|
render: (row: SeedKeyword) => (
|
||||||
|
<Badge variant="light" color="blue">{row.industry_name}</Badge>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'sector_name',
|
||||||
|
label: 'Sector',
|
||||||
|
sortable: true,
|
||||||
|
render: (row: SeedKeyword) => (
|
||||||
|
<Badge variant="light" color="green">{row.sector_name}</Badge>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'volume',
|
||||||
|
label: 'Volume',
|
||||||
|
sortable: true,
|
||||||
|
render: (row: SeedKeyword) => (
|
||||||
|
<span className="text-gray-900 dark:text-white">
|
||||||
|
{row.volume ? formatVolume(row.volume) : '-'}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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 <Badge variant="light" color={color}>{label}</Badge>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'intent',
|
||||||
|
label: 'Intent',
|
||||||
|
sortable: true,
|
||||||
|
render: (row: SeedKeyword) => (
|
||||||
|
<Badge variant="light" color="purple">{row.intent || 'N/A'}</Badge>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
], []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageMeta title="Industries, Sectors & Keywords Setup" />
|
||||||
|
<PageHeader
|
||||||
|
title="Industries, Sectors & Keywords"
|
||||||
|
badge={{ icon: <PieChartIcon />, color: 'blue' }}
|
||||||
|
hideSiteSector={true}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="p-6">
|
||||||
|
<Tabs defaultTab="industries">
|
||||||
|
{(activeTab, setActiveTab) => (
|
||||||
|
<>
|
||||||
|
<TabList className="mb-6">
|
||||||
|
<Tab tabId="industries" isActive={activeTab === 'industries'} onClick={() => setActiveTab('industries')}>
|
||||||
|
Industries
|
||||||
|
</Tab>
|
||||||
|
<Tab tabId="sectors" isActive={activeTab === 'sectors'} onClick={() => setActiveTab('sectors')} disabled={!selectedIndustry}>
|
||||||
|
Sectors {selectedIndustry && `(${selectedSectors.length})`}
|
||||||
|
</Tab>
|
||||||
|
<Tab tabId="keywords" isActive={activeTab === 'keywords'} onClick={() => setActiveTab('keywords')} disabled={!selectedIndustry}>
|
||||||
|
Keywords {selectedIndustry && `(${totalCount})`}
|
||||||
|
</Tab>
|
||||||
|
</TabList>
|
||||||
|
|
||||||
|
<TabPanel tabId="industries" isActive={activeTab === 'industries'}>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||||
|
Select Your Industry
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Choose the industry that best matches your business. This will be used as a default when creating new sites.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<div className="text-gray-500">Loading industries...</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||||
|
{industries.map((industry) => (
|
||||||
|
<Card
|
||||||
|
key={industry.slug}
|
||||||
|
className={`p-4 hover:shadow-lg transition-all duration-200 border-2 cursor-pointer ${
|
||||||
|
selectedIndustry?.slug === industry.slug
|
||||||
|
? 'border-[var(--color-primary)] bg-blue-50 dark:bg-blue-900/20'
|
||||||
|
: 'border-gray-200 dark:border-gray-700'
|
||||||
|
}`}
|
||||||
|
onClick={() => handleIndustrySelect(industry)}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-start mb-3">
|
||||||
|
<h3 className="text-base font-bold text-gray-900 dark:text-white leading-tight">
|
||||||
|
{industry.name}
|
||||||
|
</h3>
|
||||||
|
{selectedIndustry?.slug === industry.slug && (
|
||||||
|
<CheckCircleIcon className="w-5 h-5 text-[var(--color-primary)] flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{industry.description && (
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400 mb-3 line-clamp-2">
|
||||||
|
{industry.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 mb-3 text-xs">
|
||||||
|
<div className="flex items-center gap-1 text-gray-600 dark:text-gray-400">
|
||||||
|
<span className="font-medium">{industry.sectors?.length || 0}</span>
|
||||||
|
<span className="text-gray-500">sectors</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 text-gray-600 dark:text-gray-400">
|
||||||
|
<span className="font-medium">{industry.keywordsCount || 0}</span>
|
||||||
|
<span className="text-gray-500">keywords</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{industry.totalVolume > 0 && (
|
||||||
|
<div className="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-center gap-1 text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
<BoltIcon className="w-3.5 h-3.5" />
|
||||||
|
<span>Total volume: {formatVolume(industry.totalVolume)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
<TabPanel tabId="sectors" isActive={activeTab === 'sectors'}>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||||
|
Select Sectors for {selectedIndustry?.name}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Choose one or more sectors within your selected industry. These will be used as defaults when creating new sites.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!selectedIndustry ? (
|
||||||
|
<div className="text-center py-12 text-gray-500">
|
||||||
|
Please select an industry first
|
||||||
|
</div>
|
||||||
|
) : availableSectors.length === 0 ? (
|
||||||
|
<div className="text-center py-12 text-gray-500">
|
||||||
|
No sectors available for this industry
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
<TabPanel tabId="keywords" isActive={activeTab === 'keywords'}>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||||
|
Keyword Opportunities for {selectedIndustry?.name}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Browse and select keywords that match your selected industry and sectors. These will be available when creating new sites.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!selectedIndustry ? (
|
||||||
|
<div className="text-center py-12 text-gray-500">
|
||||||
|
Please select an industry first
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<TablePageTemplate
|
||||||
|
columns={keywordColumns}
|
||||||
|
data={seedKeywords}
|
||||||
|
loading={keywordsLoading}
|
||||||
|
showContent={!keywordsLoading}
|
||||||
|
filters={[
|
||||||
|
{
|
||||||
|
key: 'search',
|
||||||
|
label: 'Search',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: 'Search keywords...',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'intent',
|
||||||
|
label: 'Intent',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ value: '', label: 'All' },
|
||||||
|
{ value: 'informational', label: 'Informational' },
|
||||||
|
{ value: 'navigational', label: 'Navigational' },
|
||||||
|
{ value: 'transactional', label: 'Transactional' },
|
||||||
|
{ value: 'commercial', label: 'Commercial' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'difficulty',
|
||||||
|
label: 'Difficulty',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ value: '', label: 'All' },
|
||||||
|
{ value: '0', label: 'Easy (0-30)' },
|
||||||
|
{ value: '30', label: 'Medium (30-70)' },
|
||||||
|
{ value: '70', label: 'Hard (70-100)' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
filterValues={{
|
||||||
|
search: searchTerm,
|
||||||
|
intent: intentFilter,
|
||||||
|
difficulty: difficultyFilter,
|
||||||
|
}}
|
||||||
|
onFilterChange={(key, value) => {
|
||||||
|
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,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TabPanel>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
{/* Save Button */}
|
||||||
|
{selectedIndustry && (
|
||||||
|
<div className="mt-6 flex justify-end">
|
||||||
|
<Button
|
||||||
|
onClick={handleSavePreferences}
|
||||||
|
disabled={saving}
|
||||||
|
variant="primary"
|
||||||
|
>
|
||||||
|
{saving ? 'Saving...' : 'Save Preferences'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -5,22 +5,25 @@
|
|||||||
*/
|
*/
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
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 PageMeta from '../../components/common/PageMeta';
|
||||||
|
import PageHeader from '../../components/common/PageHeader';
|
||||||
import { Card } from '../../components/ui/card';
|
import { Card } from '../../components/ui/card';
|
||||||
import Button from '../../components/ui/button/Button';
|
import Button from '../../components/ui/button/Button';
|
||||||
|
import EnhancedMetricCard from '../../components/dashboard/EnhancedMetricCard';
|
||||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||||
import { fetchAPI, fetchSiteBlueprints } from '../../services/api';
|
import { fetchAPI, fetchSiteBlueprints } from '../../services/api';
|
||||||
import SiteProgressWidget from '../../components/sites/SiteProgressWidget';
|
import SiteProgressWidget from '../../components/sites/SiteProgressWidget';
|
||||||
|
import {
|
||||||
|
EyeIcon,
|
||||||
|
FileIcon,
|
||||||
|
PlugInIcon,
|
||||||
|
ArrowUpIcon,
|
||||||
|
CalendarIcon,
|
||||||
|
GridIcon,
|
||||||
|
BoltIcon,
|
||||||
|
PageIcon,
|
||||||
|
ArrowRightIcon
|
||||||
|
} from '../../icons';
|
||||||
|
|
||||||
interface Site {
|
interface Site {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -136,70 +139,68 @@ export default function SiteDashboard() {
|
|||||||
|
|
||||||
const statCards = [
|
const statCards = [
|
||||||
{
|
{
|
||||||
label: 'Total Pages',
|
title: 'Total Pages',
|
||||||
value: stats?.total_pages || 0,
|
value: stats?.total_pages || 0,
|
||||||
icon: <FileTextIcon className="w-5 h-5" />,
|
icon: <FileIcon />,
|
||||||
color: 'blue',
|
accentColor: 'blue' as const,
|
||||||
link: `/sites/${siteId}/pages`,
|
href: `/sites/${siteId}/pages`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Published Pages',
|
title: 'Published Pages',
|
||||||
value: stats?.published_pages || 0,
|
value: stats?.published_pages || 0,
|
||||||
icon: <GlobeIcon className="w-5 h-5" />,
|
icon: <GridIcon />,
|
||||||
color: 'green',
|
accentColor: 'success' as const,
|
||||||
link: `/sites/${siteId}/pages?status=published`,
|
href: `/sites/${siteId}/pages?status=published`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Draft Pages',
|
title: 'Draft Pages',
|
||||||
value: stats?.draft_pages || 0,
|
value: stats?.draft_pages || 0,
|
||||||
icon: <FileTextIcon className="w-5 h-5" />,
|
icon: <FileIcon />,
|
||||||
color: 'amber',
|
accentColor: 'orange' as const,
|
||||||
link: `/sites/${siteId}/pages?status=draft`,
|
href: `/sites/${siteId}/pages?status=draft`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Integrations',
|
title: 'Integrations',
|
||||||
value: stats?.integrations_count || 0,
|
value: stats?.integrations_count || 0,
|
||||||
icon: <PlugIcon className="w-5 h-5" />,
|
icon: <PlugInIcon />,
|
||||||
color: 'purple',
|
accentColor: 'purple' as const,
|
||||||
link: `/sites/${siteId}/settings?tab=integrations`,
|
href: `/sites/${siteId}/settings?tab=integrations`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Deployments',
|
title: 'Deployments',
|
||||||
value: stats?.deployments_count || 0,
|
value: stats?.deployments_count || 0,
|
||||||
icon: <TrendingUpIcon className="w-5 h-5" />,
|
icon: <ArrowUpIcon />,
|
||||||
color: 'teal',
|
accentColor: 'blue' as const,
|
||||||
link: `/sites/${siteId}/preview`,
|
href: `/sites/${siteId}/preview`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Total Content',
|
title: 'Total Content',
|
||||||
value: stats?.total_content || 0,
|
value: stats?.total_content || 0,
|
||||||
icon: <FileTextIcon className="w-5 h-5" />,
|
icon: <FileIcon />,
|
||||||
color: 'indigo',
|
accentColor: 'purple' as const,
|
||||||
link: `/sites/${siteId}/content`,
|
href: `/sites/${siteId}/content`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<PageMeta title={`${site.name} - Dashboard`} />
|
<PageMeta title={`${site.name} - Dashboard`} />
|
||||||
|
<PageHeader
|
||||||
|
title={site.name}
|
||||||
|
badge={{ icon: <GridIcon />, color: 'blue' }}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Header */}
|
{/* Site Info */}
|
||||||
<div className="mb-6 flex justify-between items-start">
|
<div className="mb-6">
|
||||||
<div>
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
{site.slug} • {site.site_type} • {site.hosting_type}
|
||||||
{site.name}
|
</p>
|
||||||
</h1>
|
{site.domain && (
|
||||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
<p className="text-sm text-gray-500 dark:text-gray-500 mt-1">
|
||||||
{site.slug} • {site.site_type} • {site.hosting_type}
|
{site.domain}
|
||||||
</p>
|
</p>
|
||||||
{site.domain && (
|
)}
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-500 mt-1">
|
<div className="flex gap-2 mt-4">
|
||||||
<GlobeIcon className="w-4 h-4 inline mr-1" />
|
|
||||||
{site.domain}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => navigate(`/sites/${siteId}/preview`)}
|
onClick={() => navigate(`/sites/${siteId}/preview`)}
|
||||||
@@ -229,87 +230,98 @@ export default function SiteDashboard() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Stats Grid */}
|
{/* Stats Grid - Using EnhancedMetricCard */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
|
||||||
{statCards.map((stat, index) => (
|
{statCards.map((stat, index) => (
|
||||||
<Card
|
<EnhancedMetricCard
|
||||||
key={index}
|
key={index}
|
||||||
className="p-4 hover:shadow-lg transition-shadow cursor-pointer"
|
title={stat.title}
|
||||||
onClick={() => stat.link && navigate(stat.link)}
|
value={stat.value}
|
||||||
>
|
icon={stat.icon}
|
||||||
<div className="flex items-center justify-between">
|
accentColor={stat.accentColor}
|
||||||
<div>
|
href={stat.href}
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-1">
|
/>
|
||||||
{stat.label}
|
|
||||||
</p>
|
|
||||||
<p className="text-2xl font-bold text-gray-900 dark:text-white">
|
|
||||||
{stat.value}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className={`text-${stat.color}-500`}>
|
|
||||||
{stat.icon}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quick Actions */}
|
|
||||||
<Card className="p-6 mb-6">
|
{/* Quick Actions - Matching Planner Dashboard pattern */}
|
||||||
|
<div className="mb-6">
|
||||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||||
Quick Actions
|
Quick Actions
|
||||||
</h2>
|
</h2>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
<Button
|
<button
|
||||||
variant="outline"
|
|
||||||
onClick={() => navigate(`/sites/${siteId}/pages`)}
|
onClick={() => navigate(`/sites/${siteId}/pages`)}
|
||||||
className="justify-start"
|
className="flex items-center gap-4 p-6 rounded-xl border-2 border-slate-200 bg-white hover:border-[var(--color-primary)] hover:shadow-lg transition-all group"
|
||||||
>
|
>
|
||||||
<FileTextIcon className="w-4 h-4 mr-2" />
|
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] flex items-center justify-center text-white shadow-lg">
|
||||||
Manage Pages
|
<PageIcon className="h-6 w-6" />
|
||||||
</Button>
|
</div>
|
||||||
<Button
|
<div className="flex-1 text-left">
|
||||||
variant="outline"
|
<h4 className="font-semibold text-slate-900 mb-1">Manage Pages</h4>
|
||||||
|
<p className="text-sm text-slate-600">View and edit pages</p>
|
||||||
|
</div>
|
||||||
|
<ArrowRightIcon className="h-5 w-5 text-slate-400 group-hover:text-[var(--color-primary)] transition" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
onClick={() => navigate(`/sites/${siteId}/content`)}
|
onClick={() => navigate(`/sites/${siteId}/content`)}
|
||||||
className="justify-start"
|
className="flex items-center gap-4 p-6 rounded-xl border-2 border-slate-200 bg-white hover:border-[var(--color-success)] hover:shadow-lg transition-all group"
|
||||||
>
|
>
|
||||||
<FileTextIcon className="w-4 h-4 mr-2" />
|
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-success)] to-[var(--color-success-dark)] flex items-center justify-center text-white shadow-lg">
|
||||||
Manage Content
|
<FileIcon className="h-6 w-6" />
|
||||||
</Button>
|
</div>
|
||||||
<Button
|
<div className="flex-1 text-left">
|
||||||
variant="outline"
|
<h4 className="font-semibold text-slate-900 mb-1">Manage Content</h4>
|
||||||
|
<p className="text-sm text-slate-600">View and edit content</p>
|
||||||
|
</div>
|
||||||
|
<ArrowRightIcon className="h-5 w-5 text-slate-400 group-hover:text-[var(--color-success)] transition" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
onClick={() => navigate(`/sites/${siteId}/settings?tab=integrations`)}
|
onClick={() => navigate(`/sites/${siteId}/settings?tab=integrations`)}
|
||||||
className="justify-start"
|
className="flex items-center gap-4 p-6 rounded-xl border-2 border-slate-200 bg-white hover:border-[var(--color-purple)] hover:shadow-lg transition-all group"
|
||||||
>
|
>
|
||||||
<PlugIcon className="w-4 h-4 mr-2" />
|
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-purple)] to-[var(--color-purple-dark)] flex items-center justify-center text-white shadow-lg">
|
||||||
Manage Integrations
|
<PlugInIcon className="h-6 w-6" />
|
||||||
</Button>
|
</div>
|
||||||
<Button
|
<div className="flex-1 text-left">
|
||||||
variant="outline"
|
<h4 className="font-semibold text-slate-900 mb-1">Integrations</h4>
|
||||||
onClick={() => navigate(`/sites/${siteId}/editor`)}
|
<p className="text-sm text-slate-600">Manage connections</p>
|
||||||
className="justify-start"
|
</div>
|
||||||
>
|
<ArrowRightIcon className="h-5 w-5 text-slate-400 group-hover:text-[var(--color-purple)] transition" />
|
||||||
<FileTextIcon className="w-4 h-4 mr-2" />
|
</button>
|
||||||
Edit Site
|
|
||||||
</Button>
|
<button
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => navigate(`/sites/${siteId}/sync`)}
|
onClick={() => navigate(`/sites/${siteId}/sync`)}
|
||||||
className="justify-start"
|
className="flex items-center gap-4 p-6 rounded-xl border-2 border-slate-200 bg-white hover:border-[var(--color-warning)] hover:shadow-lg transition-all group"
|
||||||
>
|
>
|
||||||
<RefreshCwIcon className="w-4 h-4 mr-2" />
|
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-warning)] to-[var(--color-warning-dark)] flex items-center justify-center text-white shadow-lg">
|
||||||
Sync Dashboard
|
<BoltIcon className="h-6 w-6" />
|
||||||
</Button>
|
</div>
|
||||||
<Button
|
<div className="flex-1 text-left">
|
||||||
variant="outline"
|
<h4 className="font-semibold text-slate-900 mb-1">Sync Dashboard</h4>
|
||||||
|
<p className="text-sm text-slate-600">View sync status</p>
|
||||||
|
</div>
|
||||||
|
<ArrowRightIcon className="h-5 w-5 text-slate-400 group-hover:text-[var(--color-warning)] transition" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
onClick={() => navigate(`/sites/${siteId}/deploy`)}
|
onClick={() => navigate(`/sites/${siteId}/deploy`)}
|
||||||
className="justify-start"
|
className="flex items-center gap-4 p-6 rounded-xl border-2 border-slate-200 bg-white hover:border-[var(--color-primary)] hover:shadow-lg transition-all group"
|
||||||
>
|
>
|
||||||
<RocketIcon className="w-4 h-4 mr-2" />
|
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] flex items-center justify-center text-white shadow-lg">
|
||||||
Deploy Site
|
<ArrowUpIcon className="h-6 w-6" />
|
||||||
</Button>
|
</div>
|
||||||
|
<div className="flex-1 text-left">
|
||||||
|
<h4 className="font-semibold text-slate-900 mb-1">Deploy Site</h4>
|
||||||
|
<p className="text-sm text-slate-600">Deploy to production</p>
|
||||||
|
</div>
|
||||||
|
<ArrowRightIcon className="h-5 w-5 text-slate-400 group-hover:text-[var(--color-primary)] transition" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
|
|
||||||
{/* Recent Activity */}
|
{/* Recent Activity */}
|
||||||
<Card className="p-6">
|
<Card className="p-6">
|
||||||
@@ -319,7 +331,9 @@ export default function SiteDashboard() {
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{stats?.last_deployment ? (
|
{stats?.last_deployment ? (
|
||||||
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
||||||
<CalendarIcon className="w-5 h-5 text-gray-400" />
|
<div className="flex-shrink-0 size-10 rounded-lg bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] flex items-center justify-center text-white shadow-md">
|
||||||
|
<CalendarIcon className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
<p className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
Last Deployment
|
Last Deployment
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
*/
|
*/
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
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 PageMeta from '../../components/common/PageMeta';
|
||||||
|
import PageHeader from '../../components/common/PageHeader';
|
||||||
import { Card } from '../../components/ui/card';
|
import { Card } from '../../components/ui/card';
|
||||||
import Button from '../../components/ui/button/Button';
|
import Button from '../../components/ui/button/Button';
|
||||||
import SelectDropdown from '../../components/form/SelectDropdown';
|
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 FormModal, { FormField } from '../../components/common/FormModal';
|
||||||
import Alert from '../../components/ui/alert/Alert';
|
import Alert from '../../components/ui/alert/Alert';
|
||||||
import Switch from '../../components/form/switch/Switch';
|
import Switch from '../../components/form/switch/Switch';
|
||||||
|
import {
|
||||||
|
PlusIcon,
|
||||||
|
PencilIcon,
|
||||||
|
EyeIcon,
|
||||||
|
TrashBinIcon,
|
||||||
|
GridIcon,
|
||||||
|
PlugInIcon,
|
||||||
|
FileIcon,
|
||||||
|
MoreDotIcon,
|
||||||
|
PageIcon
|
||||||
|
} from '../../icons';
|
||||||
import {
|
import {
|
||||||
fetchSites,
|
fetchSites,
|
||||||
createSite,
|
createSite,
|
||||||
@@ -503,17 +514,16 @@ export default function SiteList() {
|
|||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<PageMeta title="Sites Management - IGNY8" />
|
<PageMeta title="Sites Management - IGNY8" />
|
||||||
|
<PageHeader
|
||||||
|
title="Sites Management"
|
||||||
|
badge={{ icon: <GridIcon />, color: 'blue' }}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div>
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
Manage your sites, configure industries, and select sectors. Multiple sites can be active simultaneously.
|
||||||
Sites Management
|
</p>
|
||||||
</h1>
|
|
||||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
|
||||||
Manage your sites, configure industries, and select sectors. Multiple sites can be active simultaneously.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button onClick={() => navigate('/sites/builder')} variant="outline">
|
<Button onClick={() => navigate('/sites/builder')} variant="outline">
|
||||||
<PlusIcon className="w-4 h-4 mr-2" />
|
<PlusIcon className="w-4 h-4 mr-2" />
|
||||||
@@ -537,7 +547,9 @@ export default function SiteList() {
|
|||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
<Card className="p-4 mb-6">
|
<Card className="p-4 mb-6">
|
||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
<FilterIcon className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
<div className="flex-shrink-0 size-8 rounded-lg bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] flex items-center justify-center text-white shadow-md">
|
||||||
|
<GridIcon className="h-4 w-4" />
|
||||||
|
</div>
|
||||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
Filters
|
Filters
|
||||||
</h2>
|
</h2>
|
||||||
@@ -560,7 +572,9 @@ export default function SiteList() {
|
|||||||
Search
|
Search
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<SearchIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
|
<div className="absolute left-3 top-1/2 transform -translate-y-1/2">
|
||||||
|
<GridIcon className="w-4 h-4 text-gray-400" />
|
||||||
|
</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
@@ -645,14 +659,10 @@ export default function SiteList() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 xl:grid-cols-3">
|
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 xl:grid-cols-3">
|
||||||
{filteredSites.map((site) => (
|
{filteredSites.map((site) => (
|
||||||
<Card key={site.id} className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/3 hover:shadow-lg transition-shadow">
|
<Card key={site.id} className="rounded-xl border-2 border-slate-200 bg-white dark:border-gray-800 dark:bg-white/3 hover:border-[var(--color-primary)] hover:shadow-lg transition-all">
|
||||||
<div className="relative p-5 pb-9">
|
<div className="relative p-5 pb-9">
|
||||||
<div className="mb-5 inline-flex h-10 w-10 items-center justify-center">
|
<div className="mb-5 size-12 rounded-xl bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] flex items-center justify-center text-white shadow-lg">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40" fill="none">
|
<GridIcon className="h-6 w-6" />
|
||||||
<rect width="40" height="40" rx="8" fill="#3B82F6"/>
|
|
||||||
<path d="M12 16L20 10L28 16V28C28 28.5304 27.7893 29.0391 27.4142 29.4142C27.0391 29.7893 26.5304 30 26 30H14C13.4696 30 12.9609 29.7893 12.5858 29.4142C12.2107 29.0391 12 28.5304 12 28V16Z" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
|
||||||
<path d="M16 30V20H24V30" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<h3 className="mb-3 text-lg font-semibold text-gray-800 dark:text-white/90">
|
<h3 className="mb-3 text-lg font-semibold text-gray-800 dark:text-white/90">
|
||||||
{site.name}
|
{site.name}
|
||||||
@@ -712,7 +722,7 @@ export default function SiteList() {
|
|||||||
className="w-full justify-center text-xs"
|
className="w-full justify-center text-xs"
|
||||||
title="View Content"
|
title="View Content"
|
||||||
>
|
>
|
||||||
<FileTextIcon className="w-3 h-3 mr-1" />
|
<FileIcon className="w-3 h-3 mr-1" />
|
||||||
Content
|
Content
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -722,7 +732,7 @@ export default function SiteList() {
|
|||||||
className="w-full justify-center text-xs"
|
className="w-full justify-center text-xs"
|
||||||
title="Manage Pages"
|
title="Manage Pages"
|
||||||
>
|
>
|
||||||
<FileTextIcon className="w-3 h-3 mr-1" />
|
<PageIcon className="w-3 h-3 mr-1" />
|
||||||
Pages
|
Pages
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -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"
|
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"
|
title="Configure Sectors"
|
||||||
>
|
>
|
||||||
<Building2Icon className="w-4 h-4 mr-1" />
|
<GridIcon className="w-4 h-4 mr-1" />
|
||||||
<span className="text-xs">Sectors</span>
|
<span className="text-xs">Sectors</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -747,7 +757,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"
|
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="Site Settings"
|
title="Site Settings"
|
||||||
>
|
>
|
||||||
<SettingsIcon className="w-4 h-4 mr-1" />
|
<PlugInIcon className="w-4 h-4 mr-1" />
|
||||||
<span className="text-xs">Settings</span>
|
<span className="text-xs">Settings</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -298,9 +298,25 @@
|
|||||||
3. Add validation checks in components
|
3. Add validation checks in components
|
||||||
|
|
||||||
### Phase 4: Design Consistency - Core Sites Pages (HIGH Priority)
|
### Phase 4: Design Consistency - Core Sites Pages (HIGH Priority)
|
||||||
1. Refactor Sites Dashboard
|
|
||||||
2. Refactor Sites List
|
**Design System Requirements:**
|
||||||
3. Refactor Sites Builder pages
|
- **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)
|
### Phase 5: Design Consistency - Remaining Sites Pages (MEDIUM Priority)
|
||||||
1. Refactor Sites Settings
|
1. Refactor Sites Settings
|
||||||
|
|||||||
Reference in New Issue
Block a user