This commit is contained in:
IGNY8 VPS (Salman)
2025-11-26 12:23:28 +00:00
parent 1cbc347cdc
commit d7533934b8
7 changed files with 845 additions and 330 deletions

View File

@@ -91,7 +91,6 @@ const SiteList = lazy(() => import("./pages/Sites/List"));
const SiteManage = lazy(() => import("./pages/Sites/Manage"));
const SiteDashboard = lazy(() => import("./pages/Sites/Dashboard"));
const SiteContent = lazy(() => import("./pages/Sites/Content"));
const SiteEditor = lazy(() => import("./pages/Sites/Editor"));
const PageManager = lazy(() => import("./pages/Sites/PageManager"));
const PostEditor = lazy(() => import("./pages/Sites/PostEditor"));
const SitePreview = lazy(() => import("./pages/Sites/Preview"));
@@ -99,6 +98,8 @@ const SiteSettings = lazy(() => import("./pages/Sites/Settings"));
const SyncDashboard = lazy(() => import("./pages/Sites/SyncDashboard"));
const DeploymentPanel = lazy(() => import("./pages/Sites/DeploymentPanel"));
// Content Manager Module - Lazy loaded
const ContentManagerDashboard = lazy(() => import("./pages/ContentManager/Dashboard"));
// Help - Lazy loaded
const Help = lazy(() => import("./pages/Help/Help"));
@@ -249,6 +250,38 @@ export default function App() {
</Suspense>
} />
{/* Content Manager Module Routes */}
<Route path="/content-manager" element={
<Suspense fallback={null}>
<ContentManagerDashboard />
</Suspense>
} />
<Route path="/content-manager/posts" element={
<Suspense fallback={null}>
<ContentManagerDashboard />
</Suspense>
} />
<Route path="/content-manager/pages" element={
<Suspense fallback={null}>
<ContentManagerDashboard />
</Suspense>
} />
<Route path="/content-manager/new" element={
<Suspense fallback={null}>
<PostEditor />
</Suspense>
} />
<Route path="/content-manager/:id" element={
<Suspense fallback={null}>
<PostEditor />
</Suspense>
} />
<Route path="/content-manager/:id/edit" element={
<Suspense fallback={null}>
<PostEditor />
</Suspense>
} />
{/* Linker Module - Redirect dashboard to content */}
<Route path="/linker" element={<Navigate to="/linker/content" replace />} />
<Route path="/linker/content" element={
@@ -491,11 +524,6 @@ export default function App() {
<SiteContent />
</Suspense>
} />
<Route path="/sites/:id/editor" element={
<Suspense fallback={null}>
<SiteEditor />
</Suspense>
} />
<Route path="/sites/:id/preview" element={
<Suspense fallback={null}>
<SitePreview />

View File

@@ -14,6 +14,7 @@ import {
DocsIcon,
PageIcon,
DollarLineIcon,
FileIcon,
} from "../icons";
import { useSidebar } from "../context/SidebarContext";
import SidebarWidget from "./SidebarWidget";
@@ -133,6 +134,13 @@ const AppSidebar: React.FC = () => {
});
}
// Add Content Manager (always enabled - single item, no dropdown)
workflowItems.push({
icon: <FileIcon />,
name: "Content Manager",
path: "/content-manager", // Default to all content, submenus shown as in-page navigation
});
// Add Linker if enabled (single item, no dropdown)
if (moduleEnabled('linker')) {
workflowItems.push({

View File

@@ -0,0 +1,474 @@
/**
* Content Manager Module - Main Dashboard
* Full-featured CMS with site selector, standard filtering, and WYSIWYG editing
*/
import { useState, useEffect, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import PageMeta from '../../components/common/PageMeta';
import PageHeader from '../../components/common/PageHeader';
import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs';
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchAPI } from '../../services/api';
import { useSiteStore } from '../../store/siteStore';
import { useSectorStore } from '../../store/sectorStore';
import {
PencilIcon,
EyeIcon,
TrashBinIcon,
PlusIcon,
FileIcon
} from '../../icons';
import { Search, Filter } from 'lucide-react';
interface ContentItem {
id: number;
title: string;
status: string;
updated_at: string;
source: string;
content_type?: string;
content_structure?: string;
cluster_name?: string;
external_url?: string;
created_at: string;
}
const STATUS_OPTIONS = [
{ value: '', label: 'All Statuses' },
{ value: 'draft', label: 'Draft' },
{ value: 'published', label: 'Published' },
{ value: 'scheduled', label: 'Scheduled' },
];
const SOURCE_OPTIONS = [
{ value: '', label: 'All Sources' },
{ value: 'igny8', label: 'IGNY8 Generated' },
{ value: 'wordpress', label: 'WordPress' },
{ value: 'shopify', label: 'Shopify' },
{ value: 'custom', label: 'Custom API' },
];
const CONTENT_TYPE_OPTIONS = [
{ value: '', label: 'All Types' },
{ value: 'post', label: 'Blog Post' },
{ value: 'page', label: 'Page' },
{ value: 'product', label: 'Product' },
];
// Content type icon and color mapping
const getContentTypeStyle = (contentType?: string) => {
switch (contentType?.toLowerCase()) {
case 'post':
return {
icon: '📝',
color: 'text-blue-600 dark:text-blue-400',
bgColor: 'bg-blue-100 dark:bg-blue-900/30',
};
case 'page':
return {
icon: '📄',
color: 'text-green-600 dark:text-green-400',
bgColor: 'bg-green-100 dark:bg-green-900/30',
};
case 'product':
return {
icon: '🛍️',
color: 'text-purple-600 dark:text-purple-400',
bgColor: 'bg-purple-100 dark:bg-purple-900/30',
};
default:
return {
icon: '📋',
color: 'text-gray-600 dark:text-gray-400',
bgColor: 'bg-gray-100 dark:bg-gray-900/30',
};
}
};
// Status badge styling
const getStatusBadge = (status: string) => {
switch (status?.toLowerCase()) {
case 'published':
return 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400';
case 'draft':
return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400';
case 'scheduled':
return 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400';
default:
return 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400';
}
};
export default function ContentManagerDashboard() {
const navigate = useNavigate();
const toast = useToast();
const { activeSite } = useSiteStore();
const { activeSector } = useSectorStore();
const [content, setContent] = useState<ContentItem[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('');
const [sourceFilter, setSourceFilter] = useState('');
const [contentTypeFilter, setContentTypeFilter] = useState('');
const [sortBy, setSortBy] = useState<'created_at' | 'updated_at' | 'title'>('created_at');
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [totalCount, setTotalCount] = useState(0);
const pageSize = 20;
// Navigation tabs for Content Manager module
const navigationTabs = [
{ id: 'content', label: 'All Content', path: '/content-manager' },
{ id: 'posts', label: 'Posts', path: '/content-manager/posts' },
{ id: 'pages', label: 'Pages', path: '/content-manager/pages' },
];
useEffect(() => {
if (activeSite?.id) {
loadContent();
}
}, [activeSite, currentPage, statusFilter, sourceFilter, contentTypeFilter, searchTerm, sortBy, sortDirection]);
const loadContent = async () => {
if (!activeSite?.id) {
setLoading(false);
return;
}
try {
setLoading(true);
const params = new URLSearchParams({
site_id: activeSite.id.toString(),
page: currentPage.toString(),
page_size: pageSize.toString(),
ordering: sortDirection === 'desc' ? `-${sortBy}` : sortBy,
});
if (searchTerm) {
params.append('search', searchTerm);
}
if (statusFilter) {
params.append('status', statusFilter);
}
if (sourceFilter) {
params.append('source', sourceFilter);
}
if (contentTypeFilter) {
params.append('content_type', contentTypeFilter);
}
const data = await fetchAPI(`/v1/writer/content/?${params.toString()}`);
const contentList = Array.isArray(data?.results) ? data.results : Array.isArray(data) ? data : [];
setContent(contentList);
setTotalCount(data?.count || contentList.length);
setTotalPages(data?.total_pages || Math.ceil((data?.count || contentList.length) / pageSize));
} catch (error: any) {
toast.error(`Failed to load content: ${error.message}`);
setContent([]);
} finally {
setLoading(false);
}
};
const handleDelete = async (id: number) => {
if (!confirm('Are you sure you want to delete this content?')) return;
try {
await fetchAPI(`/v1/writer/content/${id}/`, {
method: 'DELETE',
});
toast.success('Content deleted successfully');
loadContent();
} catch (error: any) {
toast.error(`Failed to delete content: ${error.message}`);
}
};
const handleClearFilters = () => {
setSearchTerm('');
setStatusFilter('');
setSourceFilter('');
setContentTypeFilter('');
setSortBy('created_at');
setSortDirection('desc');
setCurrentPage(1);
};
const hasActiveFilters = searchTerm || statusFilter || sourceFilter || contentTypeFilter;
return (
<div className="p-6">
<PageMeta title="Content Manager - IGNY8" description="Manage all your content in one place" />
<PageHeader
title="Content Manager"
badge={{ icon: <FileIcon />, color: 'purple' }}
/>
<ModuleNavigationTabs tabs={navigationTabs} />
{/* Action Bar */}
<div className="mb-6 flex justify-between items-center">
<div className="text-sm text-gray-600 dark:text-gray-400">
{totalCount} total items
{activeSite && <span className="ml-2"> {activeSite.name}</span>}
</div>
<Button
onClick={() => activeSite ? navigate(`/sites/${activeSite.id}/posts/new`) : toast.error('Please select a site first')}
variant="primary"
startIcon={<PlusIcon className="w-4 h-4" />}
disabled={!activeSite}
>
New Content
</Button>
</div>
{/* Standard Filter Bar */}
<Card className="p-4 mb-6">
<div className="space-y-4">
{/* Search and Primary Filters */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
{/* Search */}
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="text"
placeholder="Search content..."
value={searchTerm}
onChange={(e) => {
setSearchTerm(e.target.value);
setCurrentPage(1);
}}
className="w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg dark:bg-gray-800 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
{/* Status Filter */}
<select
value={statusFilter}
onChange={(e) => {
setStatusFilter(e.target.value);
setCurrentPage(1);
}}
className="px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg dark:bg-gray-800 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
{STATUS_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
{/* Content Type Filter */}
<select
value={contentTypeFilter}
onChange={(e) => {
setContentTypeFilter(e.target.value);
setCurrentPage(1);
}}
className="px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg dark:bg-gray-800 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
{CONTENT_TYPE_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
{/* Source Filter */}
<select
value={sourceFilter}
onChange={(e) => {
setSourceFilter(e.target.value);
setCurrentPage(1);
}}
className="px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg dark:bg-gray-800 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
{SOURCE_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
{/* Sort */}
<select
value={`${sortBy}-${sortDirection}`}
onChange={(e) => {
const [field, direction] = e.target.value.split('-');
setSortBy(field as typeof sortBy);
setSortDirection(direction as 'asc' | 'desc');
}}
className="px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-lg dark:bg-gray-800 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
<option value="created_at-desc">Newest First</option>
<option value="created_at-asc">Oldest First</option>
<option value="updated_at-desc">Recently Updated</option>
<option value="title-asc">Title A-Z</option>
<option value="title-desc">Title Z-A</option>
</select>
</div>
{/* Clear Filters */}
{hasActiveFilters && (
<div className="flex justify-end">
<Button variant="ghost" size="sm" onClick={handleClearFilters}>
<Filter className="w-4 h-4 mr-2" />
Clear Filters
</Button>
</div>
)}
</div>
</Card>
{/* No Site Selected Warning */}
{!activeSite && (
<Card className="p-12 text-center">
<div className="text-gray-500 dark:text-gray-400">
<FileIcon className="w-16 h-16 mx-auto mb-4 text-gray-300 dark:text-gray-600" />
<p className="text-lg font-medium mb-2">No Site Selected</p>
<p className="text-sm">Please select a site from the dropdown above to manage content.</p>
</div>
</Card>
)}
{/* Content List */}
{activeSite && (
<>
{loading ? (
<Card className="p-12 text-center">
<div className="text-gray-500">Loading content...</div>
</Card>
) : content.length === 0 ? (
<Card className="p-12 text-center">
<p className="text-gray-600 dark:text-gray-400 mb-4">
{hasActiveFilters ? 'No content matches your filters' : 'No content found'}
</p>
{!hasActiveFilters && (
<Button onClick={() => activeSite ? navigate(`/sites/${activeSite.id}/posts/new`) : toast.error('Please select a site first')} variant="primary" disabled={!activeSite}>
Create Your First Content
</Button>
)}
</Card>
) : (
<>
<Card className="overflow-hidden">
<div className="divide-y divide-gray-200 dark:divide-gray-700">
{content.map((item) => {
const typeStyle = getContentTypeStyle(item.content_type);
return (
<div
key={item.id}
className="p-4 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors"
>
<div className="flex items-start justify-between gap-4">
{/* Content Info */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-2">
{/* Content Type Icon */}
<span className={`text-2xl ${typeStyle.bgColor} px-2 py-1 rounded`}>
{typeStyle.icon}
</span>
{/* Title */}
<h3 className="font-semibold text-gray-900 dark:text-white truncate">
{item.title || `Content #${item.id}`}
</h3>
{/* Status Badge */}
<span className={`px-2 py-1 text-xs font-medium rounded ${getStatusBadge(item.status)}`}>
{item.status}
</span>
</div>
{/* Meta Information */}
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 text-xs text-gray-500 dark:text-gray-400">
{item.content_type && (
<span className={`${typeStyle.color} font-medium`}>
{item.content_type}
</span>
)}
{item.content_structure && (
<span>{item.content_structure}</span>
)}
<span className="flex items-center gap-1">
<span className="w-2 h-2 rounded-full bg-gray-400"></span>
{item.source}
</span>
{item.cluster_name && (
<span>Cluster: {item.cluster_name}</span>
)}
<span>
Updated {new Date(item.updated_at).toLocaleDateString()}
</span>
</div>
</div>
{/* Action Buttons */}
<div className="flex gap-2 flex-shrink-0">
{/* View - Opens content detail view in Writer */}
<button
onClick={() => navigate(`/writer/content/${item.id}`)}
className="p-2 rounded-lg text-blue-600 hover:text-blue-700 hover:bg-blue-50 dark:text-blue-400 dark:hover:text-blue-300 dark:hover:bg-blue-900/20 transition-colors"
aria-label="View content"
>
<EyeIcon className="w-4 h-4" />
</button>
{/* Edit - Opens post editor */}
<button
onClick={() => navigate(`/sites/${activeSite?.id}/posts/${item.id}/edit`)}
className="p-2 rounded-lg text-green-600 hover:text-green-700 hover:bg-green-50 dark:text-green-400 dark:hover:text-green-300 dark:hover:bg-green-900/20 transition-colors"
aria-label="Edit content"
>
<PencilIcon className="w-4 h-4" />
</button>
{/* Delete */}
<button
onClick={() => handleDelete(item.id)}
className="p-2 rounded-lg text-red-600 hover:text-red-700 hover:bg-red-50 dark:text-red-400 dark:hover:text-red-300 dark:hover:bg-red-900/20 transition-colors"
aria-label="Delete content"
>
<TrashBinIcon className="w-4 h-4" />
</button>
</div>
</div>
</div>
);
})}
</div>
</Card>
{/* Pagination */}
{totalPages > 1 && (
<div className="mt-6 flex justify-center items-center gap-2">
<Button
variant="outline"
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
disabled={currentPage === 1}
>
Previous
</Button>
<span className="text-sm text-gray-600 dark:text-gray-400">
Page {currentPage} of {totalPages}
</span>
<Button
variant="outline"
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
>
Next
</Button>
</div>
)}
</>
)}
</>
)}
</div>
);
}

View File

@@ -11,11 +11,11 @@ import TablePageTemplate from '../../templates/TablePageTemplate';
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
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 ViewToggle from '../../components/common/ViewToggle';
import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs';
import WorkflowGuide from '../../components/onboarding/WorkflowGuide';
import {
PlusIcon,
PencilIcon,
@@ -25,7 +25,9 @@ import {
PlugInIcon,
FileIcon,
PageIcon,
TableIcon
TableIcon,
ChevronDownIcon,
ChevronUpIcon
} from '../../icons';
import {
fetchSites,
@@ -61,22 +63,13 @@ export default function SiteList() {
const [sites, setSites] = useState<Site[]>([]);
const [filteredSites, setFilteredSites] = useState<Site[]>([]);
const [loading, setLoading] = useState(true);
const [viewType, setViewType] = useState<ViewType>('table');
const [viewType, setViewType] = useState<ViewType>('grid');
const [showWelcomeGuide, setShowWelcomeGuide] = useState(false);
// Site Management Modals
const [selectedSite, setSelectedSite] = useState<Site | null>(null);
const [showSiteModal, setShowSiteModal] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [togglingSiteId, setTogglingSiteId] = useState<number | null>(null);
// Form state for site creation/editing
const [formData, setFormData] = useState({
name: '',
domain: '',
description: '',
is_active: true,
});
// Filters
const [searchTerm, setSearchTerm] = useState('');
const [siteTypeFilter, setSiteTypeFilter] = useState('');
@@ -187,28 +180,6 @@ export default function SiteList() {
setFilteredSites(filtered);
};
const handleCreateSite = () => {
setSelectedSite(null);
setFormData({
name: '',
domain: '',
description: '',
is_active: true,
});
setShowSiteModal(true);
};
const handleEdit = (site: Site) => {
setSelectedSite(site);
setFormData({
name: site.name || '',
domain: site.domain || '',
description: site.description || '',
is_active: site.is_active || false,
});
setShowSiteModal(true);
};
const handleSettings = (site: Site) => {
setSelectedSite(site);
setShowSectorsModal(true);
@@ -263,80 +234,6 @@ export default function SiteList() {
}
};
const handleSaveSite = async () => {
try {
setIsSaving(true);
const normalizedFormData = {
...formData,
domain: formData.domain ? normalizeDomain(formData.domain) : formData.domain,
};
if (selectedSite) {
await updateSite(selectedSite.id, normalizedFormData);
toast.success('Site updated successfully');
} else {
const newSite = await createSite({
...normalizedFormData,
is_active: normalizedFormData.is_active || false,
});
toast.success('Site created successfully');
if (sites.length === 0 || normalizedFormData.is_active) {
await setActiveSite(newSite.id);
}
}
setShowSiteModal(false);
setSelectedSite(null);
setFormData({
name: '',
domain: '',
description: '',
is_active: false,
});
await loadSites();
} catch (error: any) {
toast.error(`Failed to save site: ${error.message}`);
} finally {
setIsSaving(false);
}
};
const normalizeDomain = (domain: string): string => {
if (!domain || !domain.trim()) return domain;
const trimmed = domain.trim();
if (trimmed.startsWith('https://')) return trimmed;
if (trimmed.startsWith('http://')) return trimmed.replace('http://', 'https://');
return `https://${trimmed}`;
};
const handleSelectSectors = async () => {
if (!selectedSite || !selectedIndustry || selectedSectors.length === 0) {
toast.error('Please select an industry and at least one sector');
return;
}
if (selectedSectors.length > 5) {
toast.error('Maximum 5 sectors allowed per site');
return;
}
try {
setIsSelectingSectors(true);
await selectSectorsForSite(
selectedSite.id,
selectedIndustry,
selectedSectors
);
toast.success('Sectors selected successfully');
setShowSectorsModal(false);
await loadSites();
} catch (error: any) {
toast.error(`Failed to select sectors: ${error.message}`);
} finally {
setIsSelectingSectors(false);
}
};
const handleDeleteSite = async (siteId: number) => {
try {
await deleteSite(siteId);
@@ -347,49 +244,6 @@ export default function SiteList() {
}
};
const getSiteFormFields = (): FormField[] => [
{
key: 'name',
label: 'Site Name',
type: 'text',
value: formData.name,
onChange: (value: any) => setFormData({ ...formData, name: value }),
required: true,
placeholder: 'Enter site name',
},
{
key: 'domain',
label: 'Domain',
type: 'text',
value: formData.domain,
onChange: (value: any) => setFormData({ ...formData, domain: value }),
required: false,
placeholder: 'example.com (https:// will be added automatically)',
},
{
key: 'description',
label: 'Description',
type: 'textarea',
value: formData.description,
onChange: (value: any) => setFormData({ ...formData, description: value }),
required: false,
placeholder: 'Enter site description',
rows: 4,
},
{
key: 'is_active',
label: 'Set as Active Site',
type: 'select',
value: formData.is_active ? 'true' : 'false',
onChange: (value: any) => setFormData({ ...formData, is_active: value === 'true' }),
required: false,
options: [
{ value: 'true', label: 'Active' },
{ value: 'false', label: 'Inactive' },
],
},
];
const getIndustrySectors = () => {
if (!selectedIndustry) return [];
const industry = industries.find(i => i.slug === selectedIndustry);
@@ -527,7 +381,7 @@ export default function SiteList() {
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 xl:grid-cols-3">
{filteredSites.map((site) => (
<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-4 pb-6">
<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">
<GridIcon className="h-6 w-6" />
</div>
@@ -549,16 +403,18 @@ export default function SiteList() {
{site.industry_name}
</Badge>
)}
<Badge variant="light" color="info" className="text-xs">
{site.active_sectors_count || 0} / 5 Sectors
</Badge>
{site.integration_count && site.integration_count > 0 && (
<Badge variant="soft" color="success" size="xs">
<Badge variant="soft" color="success" className="text-[10px] px-1.5 py-0.5">
{site.integration_count} integration{site.integration_count > 1 ? 's' : ''}
</Badge>
)}
</div>
<div className="absolute top-5 right-5">
<div className="absolute top-4 right-4 flex items-center gap-3">
<Switch
checked={site.is_active}
onChange={(enabled) => handleToggle(site.id, enabled)}
disabled={togglingSiteId === site.id}
/>
<Badge
variant={site.is_active ? "soft" : "light"}
color={site.is_active ? "success" : "gray"}
@@ -568,8 +424,8 @@ export default function SiteList() {
</Badge>
</div>
</div>
<div className="border-t border-gray-200 p-5 dark:border-gray-800">
<div className="grid grid-cols-3 gap-2 mb-3">
<div className="border-t border-gray-200 p-3 dark:border-gray-800">
<div className="grid grid-cols-2 gap-2">
<Button
onClick={() => navigate(`/sites/${site.id}`)}
variant="primary"
@@ -587,31 +443,15 @@ export default function SiteList() {
Content
</Button>
<Button
onClick={() => navigate(`/sites/${site.id}/pages`)}
onClick={() => navigate(`/sites/${site.id}/settings`)}
variant="outline"
size="sm"
startIcon={<PageIcon className="w-4 h-4" />}
startIcon={<PlugInIcon className="w-4 h-4" />}
className="col-span-2"
>
Pages
Settings
</Button>
</div>
<div className="flex items-center justify-between">
<div className="flex gap-2">
<Button
onClick={() => navigate(`/sites/${site.id}/settings`)}
variant="outline"
size="sm"
startIcon={<PlugInIcon className="w-4 h-4" />}
>
Settings
</Button>
</div>
<Switch
checked={site.is_active}
onChange={(enabled) => handleToggle(site.id, enabled)}
disabled={togglingSiteId === site.id}
/>
</div>
</div>
</Card>
))}
@@ -634,8 +474,6 @@ export default function SiteList() {
// Navigation tabs for Sites module
const sitesTabs = [
{ label: 'All Sites', path: '/sites', icon: <TableIcon className="w-4 h-4" /> },
{ label: 'Create Site', path: '/sites/builder', icon: <PlusIcon className="w-4 h-4" /> },
{ label: 'Blueprints', path: '/sites/blueprints', icon: <FileIcon className="w-4 h-4" /> },
];
return (
@@ -648,25 +486,19 @@ export default function SiteList() {
navigation={<ModuleNavigationTabs tabs={sitesTabs} />}
/>
{/* Info Alert */}
<div className="mb-6">
<Alert
variant="info"
title="Sites Configuration"
message="Each site can have up to 5 sectors selected from 15 major industries. Keywords and clusters are automatically associated with sectors. Multiple sites can be active simultaneously."
/>
</div>
{/* Custom Header Actions - Create buttons and view toggle */}
{/* Custom Header Actions - Add Site button and view toggle */}
<div className="flex items-center justify-between mb-6">
<div className="flex-1"></div>
<div className="flex items-center gap-3">
<Button onClick={() => navigate('/sites/builder')} variant="outline" startIcon={<PlusIcon className="w-4 h-4" />}>
Create with Builder
</Button>
<Button onClick={handleCreateSite} variant="primary" startIcon={<PlusIcon className="w-4 h-4" />}>
<div className="flex-1">
<Button
onClick={() => setShowWelcomeGuide(!showWelcomeGuide)}
variant="success"
size="md"
startIcon={<PlusIcon className="w-5 h-5" />}
>
Add Site
</Button>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<Button
onClick={() => setViewType('table')}
@@ -687,6 +519,16 @@ export default function SiteList() {
</div>
</div>
</div>
{/* Welcome Guide - Collapsible */}
{showWelcomeGuide && (
<div className="mb-6">
<WorkflowGuide onSiteAdded={() => {
loadSites();
setShowWelcomeGuide(false);
}} />
</div>
)}
{/* Table View */}
{viewType === 'table' ? (
@@ -742,7 +584,6 @@ export default function SiteList() {
else if (key === 'integration') setIntegrationFilter(value);
}}
onFilterReset={clearFilters}
onEdit={(row) => handleEdit(row)}
onDelete={async (id) => {
await handleDeleteSite(id);
}}
@@ -750,77 +591,61 @@ export default function SiteList() {
/>
) : (
<>
{/* Filters for Grid View */}
<Card className="p-4 mb-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
<div className="lg:col-span-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Search
</label>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search sites..."
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Site Type
</label>
<select
value={siteTypeFilter}
onChange={(e) => setSiteTypeFilter(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
>
{SITE_TYPES.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Hosting
</label>
<select
value={hostingTypeFilter}
onChange={(e) => setHostingTypeFilter(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
>
{HOSTING_TYPES.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Status
</label>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
>
{STATUS_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
{/* Standard Filters Bar for Grid View - Matches Table View */}
<div className="flex justify-center mb-4">
<div
className="w-[75%] igny8-filter-bar p-3 rounded-lg bg-transparent"
style={{ boxShadow: '0 2px 6px 3px rgba(0, 0, 0, 0.08)' }}
>
<div className="flex flex-nowrap gap-3 items-center justify-between w-full">
<div className="flex flex-nowrap gap-3 items-center flex-1 min-w-0 w-full">
<input
type="text"
placeholder="Search sites..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="flex-1 min-w-[200px] h-9 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<select
value={siteTypeFilter}
onChange={(e) => setSiteTypeFilter(e.target.value)}
className="flex-1 min-w-[140px] h-9 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{SITE_TYPES.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
<select
value={hostingTypeFilter}
onChange={(e) => setHostingTypeFilter(e.target.value)}
className="flex-1 min-w-[140px] h-9 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{HOSTING_TYPES.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="flex-1 min-w-[140px] h-9 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{STATUS_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
</div>
{hasActiveFilters && (
<Button
variant="secondary"
size="sm"
onClick={clearFilters}
className="flex-shrink-0"
>
Clear Filters
</Button>
)}
</div>
</div>
{hasActiveFilters && (
<div className="mt-4">
<Button variant="ghost" size="sm" onClick={clearFilters}>
Clear Filters
</Button>
</div>
)}
</Card>
{/* Results Count */}
<div className="mb-4 text-sm text-gray-600 dark:text-gray-400">
Showing {filteredSites.length} of {sites.length} sites
{hasActiveFilters && ' (filtered)'}
</div>
{/* Grid View */}
@@ -834,8 +659,8 @@ export default function SiteList() {
Clear Filters
</Button>
) : (
<Button onClick={handleCreateSite} variant="primary">
Create Your First Site
<Button onClick={() => setShowWelcomeGuide(true)} variant="success" startIcon={<PlusIcon className="w-5 h-5" />}>
Add Your First Site
</Button>
)}
</Card>
@@ -844,26 +669,6 @@ export default function SiteList() {
)}
</>
)}
{/* Create/Edit Site Modal */}
<FormModal
isOpen={showSiteModal}
onClose={() => {
setShowSiteModal(false);
setSelectedSite(null);
setFormData({
name: '',
domain: '',
description: '',
is_active: false,
});
}}
onSubmit={handleSaveSite}
title={selectedSite ? 'Edit Site' : 'Create New Site'}
submitLabel={selectedSite ? 'Update Site' : 'Create Site'}
fields={getSiteFormFields()}
isLoading={isSaving}
/>
</div>
);
}