679 lines
23 KiB
TypeScript
679 lines
23 KiB
TypeScript
/**
|
|
* Site List View
|
|
* Refactored to use TablePageTemplate with table view as default
|
|
* Supports table and grid view toggle
|
|
*/
|
|
import React, { useState, useEffect, useMemo } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import PageMeta from '../../components/common/PageMeta';
|
|
import PageHeader from '../../components/common/PageHeader';
|
|
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 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,
|
|
EyeIcon,
|
|
TrashBinIcon,
|
|
GridIcon,
|
|
PlugInIcon,
|
|
FileIcon,
|
|
PageIcon,
|
|
TableIcon,
|
|
ChevronDownIcon,
|
|
ChevronUpIcon
|
|
} from '../../icons';
|
|
import {
|
|
fetchSites,
|
|
createSite,
|
|
updateSite,
|
|
deleteSite,
|
|
setActiveSite,
|
|
selectSectorsForSite,
|
|
fetchIndustries,
|
|
fetchSiteSectors,
|
|
Site as SiteType,
|
|
Industry,
|
|
fetchAPI,
|
|
} from '../../services/api';
|
|
import { useToast } from '../../components/ui/toast/ToastContainer';
|
|
import SiteTypeBadge from '../../components/sites/SiteTypeBadge';
|
|
|
|
interface Site extends SiteType {
|
|
page_count?: number;
|
|
integration_count?: number;
|
|
has_wordpress_integration?: boolean;
|
|
domain?: string;
|
|
description?: string;
|
|
industry_name?: string;
|
|
active_sectors_count?: number;
|
|
}
|
|
|
|
type ViewType = 'table' | 'grid';
|
|
|
|
export default function SiteList() {
|
|
const navigate = useNavigate();
|
|
const toast = useToast();
|
|
const [sites, setSites] = useState<Site[]>([]);
|
|
const [filteredSites, setFilteredSites] = useState<Site[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [viewType, setViewType] = useState<ViewType>('grid');
|
|
const [showWelcomeGuide, setShowWelcomeGuide] = useState(false);
|
|
|
|
// Site Management Modals
|
|
const [selectedSite, setSelectedSite] = useState<Site | null>(null);
|
|
const [togglingSiteId, setTogglingSiteId] = useState<number | null>(null);
|
|
|
|
// Filters
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [siteTypeFilter, setSiteTypeFilter] = useState('');
|
|
const [hostingTypeFilter, setHostingTypeFilter] = useState('');
|
|
const [statusFilter, setStatusFilter] = useState('');
|
|
const [integrationFilter, setIntegrationFilter] = useState('');
|
|
|
|
useEffect(() => {
|
|
loadSites();
|
|
}, []);
|
|
|
|
const loadUserPreferences = async () => {
|
|
try {
|
|
const { fetchAccountSetting } = await import('../../services/api');
|
|
const setting = await fetchAccountSetting('user_preferences');
|
|
const preferences = setting.config as { selectedIndustry?: string; selectedSectors?: string[] } | undefined;
|
|
if (preferences) {
|
|
setUserPreferences(preferences);
|
|
}
|
|
} catch (error: any) {
|
|
// 404 means preferences don't exist yet - that's fine
|
|
// 500 and other errors should be handled silently - user can still use the page
|
|
if (error?.status === 404) {
|
|
// Preferences don't exist yet - this is expected for new users
|
|
return;
|
|
}
|
|
// Silently handle other errors (500, network errors, etc.) - don't spam console
|
|
// User can still use the page without preferences
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
applyFilters();
|
|
}, [sites, searchTerm, siteTypeFilter, hostingTypeFilter, statusFilter, integrationFilter]);
|
|
|
|
const loadSites = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const response = await fetchSites();
|
|
const data = response.results || response || [];
|
|
if (Array.isArray(data)) {
|
|
// Check for WordPress integrations
|
|
const sitesWithIntegrations = await Promise.all(
|
|
data.map(async (site: Site) => {
|
|
if (site.hosting_type === 'wordpress') {
|
|
try {
|
|
const integrations = await fetchAPI(`/v1/integration/integrations/?site=${site.id}&platform=wordpress`);
|
|
return {
|
|
...site,
|
|
has_wordpress_integration: integrations?.results?.length > 0 || integrations?.length > 0,
|
|
};
|
|
} catch {
|
|
return { ...site, has_wordpress_integration: false };
|
|
}
|
|
}
|
|
return site;
|
|
})
|
|
);
|
|
setSites(sitesWithIntegrations);
|
|
}
|
|
} catch (error: any) {
|
|
toast.error(`Failed to load sites: ${error.message}`);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const applyFilters = () => {
|
|
let filtered = [...sites];
|
|
|
|
// Search filter
|
|
if (searchTerm) {
|
|
filtered = filtered.filter(
|
|
(site) =>
|
|
site.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
site.slug.toLowerCase().includes(searchTerm.toLowerCase())
|
|
);
|
|
}
|
|
|
|
// Site type filter
|
|
if (siteTypeFilter) {
|
|
filtered = filtered.filter((site) => site.site_type === siteTypeFilter);
|
|
}
|
|
|
|
// Hosting type filter
|
|
if (hostingTypeFilter) {
|
|
filtered = filtered.filter((site) => site.hosting_type === hostingTypeFilter);
|
|
}
|
|
|
|
// Status filter
|
|
if (statusFilter) {
|
|
if (statusFilter === 'active') {
|
|
filtered = filtered.filter((site) => site.is_active);
|
|
} else if (statusFilter === 'inactive') {
|
|
filtered = filtered.filter((site) => !site.is_active);
|
|
} else {
|
|
filtered = filtered.filter((site) => site.status === statusFilter);
|
|
}
|
|
}
|
|
|
|
// Integration filter
|
|
if (integrationFilter === 'has_integrations') {
|
|
filtered = filtered.filter((site) => (site.integration_count || 0) > 0);
|
|
} else if (integrationFilter === 'no_integrations') {
|
|
filtered = filtered.filter((site) => (site.integration_count || 0) === 0);
|
|
}
|
|
|
|
setFilteredSites(filtered);
|
|
};
|
|
|
|
const handleSettings = (site: Site) => {
|
|
setSelectedSite(site);
|
|
setShowSectorsModal(true);
|
|
loadSiteSectors(site);
|
|
};
|
|
|
|
const handleToggle = async (siteId: number, enabled: boolean) => {
|
|
if (togglingSiteId !== null) {
|
|
toast.error('Please wait for the current operation to complete');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setTogglingSiteId(siteId);
|
|
if (enabled) {
|
|
await setActiveSite(siteId);
|
|
toast.success('Site activated successfully');
|
|
} else {
|
|
const site = sites.find(s => s.id === siteId);
|
|
if (site) {
|
|
await updateSite(siteId, { is_active: false });
|
|
toast.success('Site deactivated successfully');
|
|
}
|
|
}
|
|
await loadSites();
|
|
} catch (error: any) {
|
|
toast.error(`Failed to update site: ${error.message}`);
|
|
} finally {
|
|
setTogglingSiteId(null);
|
|
}
|
|
};
|
|
|
|
const loadSiteSectors = async (site: Site) => {
|
|
try {
|
|
const sectors = await fetchSiteSectors(site.id);
|
|
const sectorSlugs = sectors.map((s: any) => s.slug);
|
|
setSelectedSectors(sectorSlugs);
|
|
|
|
if (site.industry_slug) {
|
|
setSelectedIndustry(site.industry_slug);
|
|
} else {
|
|
for (const industry of industries) {
|
|
const matchingSectors = industry.sectors.filter(s => sectorSlugs.includes(s.slug));
|
|
if (matchingSectors.length > 0) {
|
|
setSelectedIndustry(industry.slug);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} catch (error: any) {
|
|
console.error('Failed to load site sectors:', error);
|
|
}
|
|
};
|
|
|
|
const handleDeleteSite = async (siteId: number) => {
|
|
try {
|
|
await deleteSite(siteId);
|
|
toast.success('Site deleted successfully');
|
|
await loadSites();
|
|
} catch (error: any) {
|
|
toast.error(`Failed to delete site: ${error.message}`);
|
|
}
|
|
};
|
|
|
|
const getIndustrySectors = () => {
|
|
if (!selectedIndustry) return [];
|
|
const industry = industries.find(i => i.slug === selectedIndustry);
|
|
let sectors = industry?.sectors || [];
|
|
|
|
// Filter to show only user's pre-selected sectors from account preferences
|
|
if (userPreferences?.selectedSectors && userPreferences.selectedSectors.length > 0) {
|
|
sectors = sectors.filter(s => userPreferences.selectedSectors!.includes(s.slug));
|
|
}
|
|
|
|
return sectors;
|
|
};
|
|
|
|
const clearFilters = () => {
|
|
setSearchTerm('');
|
|
setSiteTypeFilter('');
|
|
setHostingTypeFilter('');
|
|
setStatusFilter('');
|
|
setIntegrationFilter('');
|
|
};
|
|
|
|
const SITE_TYPES = [
|
|
{ value: '', label: 'All Types' },
|
|
{ value: 'marketing', label: 'Marketing' },
|
|
{ value: 'ecommerce', label: 'Ecommerce' },
|
|
{ value: 'blog', label: 'Blog' },
|
|
{ value: 'portfolio', label: 'Portfolio' },
|
|
{ value: 'corporate', label: 'Corporate' },
|
|
];
|
|
|
|
const HOSTING_TYPES = [
|
|
{ value: '', label: 'All Hosting' },
|
|
{ value: 'igny8_sites', label: 'IGNY8 Sites' },
|
|
{ value: 'wordpress', label: 'WordPress' },
|
|
{ value: 'shopify', label: 'Shopify' },
|
|
{ value: 'multi', label: 'Multi-Destination' },
|
|
];
|
|
|
|
const STATUS_OPTIONS = [
|
|
{ value: '', label: 'All Status' },
|
|
{ value: 'active', label: 'Active' },
|
|
{ value: 'inactive', label: 'Inactive' },
|
|
];
|
|
|
|
const INTEGRATION_OPTIONS = [
|
|
{ value: '', label: 'All Sites' },
|
|
{ value: 'has_integrations', label: 'Has Integrations' },
|
|
{ value: 'no_integrations', label: 'No Integrations' },
|
|
];
|
|
|
|
// Table columns configuration
|
|
const tableColumns = useMemo(() => [
|
|
{
|
|
key: 'name',
|
|
label: 'Site Name',
|
|
sortable: true,
|
|
render: (value: string, row: Site) => (
|
|
<div className="flex items-center gap-3">
|
|
<div className="size-10 rounded-lg bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] flex items-center justify-center text-white flex-shrink-0">
|
|
<GridIcon className="h-5 w-5" />
|
|
</div>
|
|
<div>
|
|
<div className="font-semibold text-gray-900 dark:text-white">{row.name}</div>
|
|
{row.domain && (
|
|
<div className="text-xs text-gray-500 dark:text-gray-400">{row.domain}</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
key: 'hosting_type',
|
|
label: 'Hosting',
|
|
sortable: true,
|
|
render: (value: string, row: Site) => <SiteTypeBadge hostingType={row.hosting_type} />,
|
|
},
|
|
{
|
|
key: 'industry_name',
|
|
label: 'Industry',
|
|
sortable: false,
|
|
render: (value: string, row: Site) => (
|
|
row.industry_name ? (
|
|
<Badge variant="light" color="info" size="sm">
|
|
{row.industry_name}
|
|
</Badge>
|
|
) : (
|
|
<span className="text-gray-400">-</span>
|
|
)
|
|
),
|
|
},
|
|
{
|
|
key: 'active_sectors_count',
|
|
label: 'Sectors',
|
|
sortable: false,
|
|
render: (value: number, row: Site) => (
|
|
<Badge variant="light" color="info" size="sm">
|
|
{row.active_sectors_count || 0} / 5
|
|
</Badge>
|
|
),
|
|
},
|
|
{
|
|
key: 'integration_count',
|
|
label: 'Integrations',
|
|
sortable: false,
|
|
render: (value: number, row: Site) => (
|
|
row.integration_count && row.integration_count > 0 ? (
|
|
<Badge variant="soft" color="success" size="sm">
|
|
{row.integration_count}
|
|
</Badge>
|
|
) : (
|
|
<span className="text-gray-400">-</span>
|
|
)
|
|
),
|
|
},
|
|
{
|
|
key: 'is_active',
|
|
label: 'Status',
|
|
sortable: true,
|
|
render: (value: boolean, row: Site) => (
|
|
<div className="flex items-center gap-2">
|
|
<Badge
|
|
variant={row.is_active ? "soft" : "light"}
|
|
color={row.is_active ? "success" : "gray"}
|
|
size="sm"
|
|
>
|
|
{row.is_active ? 'Active' : 'Inactive'}
|
|
</Badge>
|
|
</div>
|
|
),
|
|
},
|
|
], []);
|
|
|
|
// Grid view component
|
|
const renderGridView = () => (
|
|
<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-4 pb-6">
|
|
<div className="flex items-center gap-3 mb-4">
|
|
<div className="size-6 rounded-lg bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] flex items-center justify-center text-white shadow-md flex-shrink-0">
|
|
<svg className="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
</div>
|
|
<h3 className="text-lg font-semibold text-gray-800 dark:text-white/90">
|
|
{site.name}
|
|
</h3>
|
|
</div>
|
|
<p className="max-w-xs text-sm text-gray-500 dark:text-gray-400 mb-2">
|
|
{site.description || 'No description'}
|
|
</p>
|
|
{site.domain && (
|
|
<p className="text-xs text-gray-400 dark:text-gray-500 mb-2">
|
|
{site.domain}
|
|
</p>
|
|
)}
|
|
<div className="flex items-center gap-2 mb-2 flex-wrap">
|
|
<SiteTypeBadge hostingType={site.hosting_type} />
|
|
{site.industry_name && (
|
|
<Badge variant="light" color="info" className="text-xs">
|
|
{site.industry_name}
|
|
</Badge>
|
|
)}
|
|
{site.integration_count && site.integration_count > 0 && (
|
|
<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-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"}
|
|
size="sm"
|
|
>
|
|
{site.is_active ? 'Active' : 'Inactive'}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
<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"
|
|
size="sm"
|
|
startIcon={<EyeIcon className="w-4 h-4" />}
|
|
>
|
|
Dashboard
|
|
</Button>
|
|
<Button
|
|
onClick={() => navigate(`/sites/${site.id}/content`)}
|
|
variant="secondary"
|
|
size="sm"
|
|
startIcon={<FileIcon className="w-4 h-4" />}
|
|
>
|
|
Content
|
|
</Button>
|
|
<Button
|
|
onClick={() => navigate(`/sites/${site.id}/settings`)}
|
|
variant="outline"
|
|
size="sm"
|
|
startIcon={<PlugInIcon className="w-4 h-4" />}
|
|
className="col-span-2"
|
|
>
|
|
Settings
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
);
|
|
|
|
const hasActiveFilters = searchTerm || siteTypeFilter || hostingTypeFilter || statusFilter || integrationFilter;
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="p-6">
|
|
<PageMeta title="Site List" />
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="text-gray-500">Loading sites...</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Navigation tabs for Sites module
|
|
const sitesTabs = [
|
|
{ label: 'All Sites', path: '/sites', icon: <TableIcon className="w-4 h-4" /> },
|
|
];
|
|
|
|
return (
|
|
<div className="p-6">
|
|
<PageMeta title="Sites Management - IGNY8" />
|
|
<PageHeader
|
|
title="Sites Management"
|
|
badge={{ icon: <GridIcon />, color: 'blue' }}
|
|
hideSiteSector={true}
|
|
navigation={<ModuleNavigationTabs tabs={sitesTabs} />}
|
|
/>
|
|
|
|
{/* Custom Header Actions - Add Site button and view toggle */}
|
|
<div className="flex items-center justify-between mb-6">
|
|
<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')}
|
|
variant={viewType === 'table' ? 'secondary' : 'ghost'}
|
|
size="sm"
|
|
startIcon={<TableIcon className="w-4 h-4" />}
|
|
>
|
|
<span className="hidden sm:inline">Table</span>
|
|
</Button>
|
|
<Button
|
|
onClick={() => setViewType('grid')}
|
|
variant={viewType === 'grid' ? 'secondary' : 'ghost'}
|
|
size="sm"
|
|
startIcon={<GridIcon className="w-4 h-4" />}
|
|
>
|
|
<span className="hidden sm:inline">Grid</span>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Welcome Guide - Collapsible */}
|
|
{showWelcomeGuide && (
|
|
<div className="mb-6">
|
|
<WorkflowGuide onSiteAdded={() => {
|
|
loadSites();
|
|
setShowWelcomeGuide(false);
|
|
}} />
|
|
</div>
|
|
)}
|
|
|
|
{/* Table View */}
|
|
{viewType === 'table' ? (
|
|
<TablePageTemplate
|
|
columns={tableColumns}
|
|
data={filteredSites}
|
|
loading={loading}
|
|
showContent={!loading}
|
|
filters={[
|
|
{
|
|
key: 'search',
|
|
label: 'Search',
|
|
type: 'text',
|
|
placeholder: 'Search sites...',
|
|
},
|
|
{
|
|
key: 'site_type',
|
|
label: 'Site Type',
|
|
type: 'select',
|
|
options: SITE_TYPES,
|
|
},
|
|
{
|
|
key: 'hosting_type',
|
|
label: 'Hosting',
|
|
type: 'select',
|
|
options: HOSTING_TYPES,
|
|
},
|
|
{
|
|
key: 'status',
|
|
label: 'Status',
|
|
type: 'select',
|
|
options: STATUS_OPTIONS,
|
|
},
|
|
{
|
|
key: 'integration',
|
|
label: 'Integrations',
|
|
type: 'select',
|
|
options: INTEGRATION_OPTIONS,
|
|
},
|
|
]}
|
|
filterValues={{
|
|
search: searchTerm,
|
|
site_type: siteTypeFilter,
|
|
hosting_type: hostingTypeFilter,
|
|
status: statusFilter,
|
|
integration: integrationFilter,
|
|
}}
|
|
onFilterChange={(key, value) => {
|
|
if (key === 'search') setSearchTerm(value);
|
|
else if (key === 'site_type') setSiteTypeFilter(value);
|
|
else if (key === 'hosting_type') setHostingTypeFilter(value);
|
|
else if (key === 'status') setStatusFilter(value);
|
|
else if (key === 'integration') setIntegrationFilter(value);
|
|
}}
|
|
onFilterReset={clearFilters}
|
|
onDelete={async (id) => {
|
|
await handleDeleteSite(id);
|
|
}}
|
|
getItemDisplayName={(row) => row.name}
|
|
/>
|
|
) : (
|
|
<>
|
|
{/* 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>
|
|
</div>
|
|
|
|
{/* Grid View */}
|
|
{filteredSites.length === 0 ? (
|
|
<Card className="p-12 text-center">
|
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
|
{hasActiveFilters ? 'No sites match your filters' : 'No sites created yet'}
|
|
</p>
|
|
{hasActiveFilters ? (
|
|
<Button onClick={clearFilters} variant="outline">
|
|
Clear Filters
|
|
</Button>
|
|
) : (
|
|
<Button onClick={() => setShowWelcomeGuide(true)} variant="success" startIcon={<PlusIcon className="w-5 h-5" />}>
|
|
Add Your First Site
|
|
</Button>
|
|
)}
|
|
</Card>
|
|
) : (
|
|
renderGridView()
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|