This commit is contained in:
alorig
2025-11-20 09:34:54 +05:00
parent 09232aa1c0
commit 8489b2ea48
5 changed files with 751 additions and 386 deletions

View File

@@ -1,22 +1,20 @@
/**
* Site List View
* Phase 7: UI Components & Prompt Management
* Advanced site list with filters and search
* Refactored to use TablePageTemplate with table view as default
* Supports table and grid view toggle
*/
import React, { useState, useEffect } from 'react';
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 SelectDropdown from '../../components/form/SelectDropdown';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchAPI } from '../../services/api';
import SiteTypeBadge from '../../components/sites/SiteTypeBadge';
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 {
PlusIcon,
PencilIcon,
@@ -25,8 +23,8 @@ import {
GridIcon,
PlugInIcon,
FileIcon,
MoreDotIcon,
PageIcon
PageIcon,
TableIcon
} from '../../icons';
import {
fetchSites,
@@ -39,7 +37,10 @@ import {
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;
@@ -51,18 +52,20 @@ interface Site extends SiteType {
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>('table');
// Site Management Modals
const [selectedSite, setSelectedSite] = useState<Site | null>(null);
const [showSiteModal, setShowSiteModal] = useState(false);
const [showSectorsModal, setShowSectorsModal] = useState(false);
const [showDetailsModal, setShowDetailsModal] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [togglingSiteId, setTogglingSiteId] = useState<number | null>(null);
const [industries, setIndustries] = useState<Industry[]>([]);
@@ -168,7 +171,7 @@ export default function SiteList() {
}
}
// Integration filter (has integrations or not)
// Integration filter
if (integrationFilter === 'has_integrations') {
filtered = filtered.filter((site) => (site.integration_count || 0) > 0);
} else if (integrationFilter === 'no_integrations') {
@@ -189,18 +192,7 @@ export default function SiteList() {
setShowSiteModal(true);
};
const handleIntegration = (siteId: number) => {
navigate(`/sites/${siteId}/settings?tab=integrations`);
};
const handleEdit = (siteId: number) => {
const site = sites.find(s => s.id === siteId);
if (site) {
handleEditSite(site);
}
};
const handleEditSite = (site: Site) => {
const handleEdit = (site: Site) => {
setSelectedSite(site);
setFormData({
name: site.name || '',
@@ -211,13 +203,10 @@ export default function SiteList() {
setShowSiteModal(true);
};
const handleSettings = (siteId: number) => {
const site = sites.find(s => s.id === siteId);
if (site) {
setSelectedSite(site);
setShowSectorsModal(true);
loadSiteSectors(site);
}
const handleSettings = (site: Site) => {
setSelectedSite(site);
setShowSectorsModal(true);
loadSiteSectors(site);
};
const handleToggle = async (siteId: number, enabled: boolean) => {
@@ -268,45 +257,6 @@ export default function SiteList() {
}
};
const handleDetails = (site: Site) => {
setSelectedSite(site);
setFormData({
name: site.name || '',
domain: site.domain || '',
description: site.description || '',
is_active: site.is_active || false,
});
setShowDetailsModal(true);
};
const handleSaveDetails = async () => {
if (!selectedSite) return;
try {
setIsSaving(true);
const normalizedFormData = {
...formData,
domain: formData.domain ? normalizeDomain(formData.domain) : formData.domain,
};
await updateSite(selectedSite.id, normalizedFormData);
toast.success('Site updated successfully');
setShowDetailsModal(false);
await loadSites();
} catch (error: any) {
toast.error(`Failed to update 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 handleSaveSite = async () => {
try {
setIsSaving(true);
@@ -345,6 +295,14 @@ export default function SiteList() {
}
};
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');
@@ -373,19 +331,11 @@ export default function SiteList() {
}
};
const handleDeleteSite = async (site: Site) => {
if (!window.confirm(`Are you sure you want to delete "${site.name}"? This action cannot be undone.`)) {
return;
}
const handleDeleteSite = async (siteId: number) => {
try {
await deleteSite(site.id);
await deleteSite(siteId);
toast.success('Site deleted successfully');
await loadSites();
if (showDetailsModal) {
setShowDetailsModal(false);
setSelectedSite(null);
}
} catch (error: any) {
toast.error(`Failed to delete site: ${error.message}`);
}
@@ -440,24 +390,6 @@ export default function SiteList() {
return industry?.sectors || [];
};
const handleView = (siteId: number) => {
navigate(`/sites/${siteId}`);
};
const handleDelete = async (siteId: number) => {
if (!confirm('Are you sure you want to delete this site?')) return;
try {
await fetchAPI(`/v1/auth/sites/${siteId}/`, {
method: 'DELETE',
});
toast.success('Site deleted successfully');
loadSites();
} catch (error: any) {
toast.error(`Failed to delete site: ${error.message}`);
}
};
const clearFilters = () => {
setSearchTerm('');
setSiteTypeFilter('');
@@ -487,9 +419,6 @@ export default function SiteList() {
{ value: '', label: 'All Status' },
{ value: 'active', label: 'Active' },
{ value: 'inactive', label: 'Inactive' },
{ value: 'active', label: 'Active Status' },
{ value: 'inactive', label: 'Inactive Status' },
{ value: 'suspended', label: 'Suspended' },
];
const INTEGRATION_OPTIONS = [
@@ -498,6 +427,197 @@ export default function SiteList() {
{ 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-5 pb-9">
<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>
<h3 className="mb-3 text-lg font-semibold text-gray-800 dark:text-white/90">
{site.name}
</h3>
<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>
)}
<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">
{site.integration_count} integration{site.integration_count > 1 ? 's' : ''}
</Badge>
)}
</div>
<div className="absolute top-5 right-5">
<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-5 dark:border-gray-800">
<div className="grid grid-cols-3 gap-2 mb-3">
<Button
variant="outline"
size="sm"
onClick={() => navigate(`/sites/${site.id}`)}
className="w-full justify-center text-xs"
>
<EyeIcon className="w-3 h-3 mr-1" />
Dashboard
</Button>
<Button
variant="outline"
size="sm"
onClick={() => navigate(`/sites/${site.id}/content`)}
className="w-full justify-center text-xs"
>
<FileIcon className="w-3 h-3 mr-1" />
Content
</Button>
<Button
variant="outline"
size="sm"
onClick={() => navigate(`/sites/${site.id}/pages`)}
className="w-full justify-center text-xs"
>
<PageIcon className="w-3 h-3 mr-1" />
Pages
</Button>
</div>
<div className="flex items-center justify-between">
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => handleSettings(site)}
title="Configure Sectors"
>
<GridIcon className="w-4 h-4 mr-1" />
<span className="text-xs">Sectors</span>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => navigate(`/sites/${site.id}/settings`)}
title="Site Settings"
>
<PlugInIcon className="w-4 h-4 mr-1" />
<span className="text-xs">Settings</span>
</Button>
</div>
<Switch
checked={site.is_active}
onChange={(enabled) => handleToggle(site.id, enabled)}
disabled={togglingSiteId === site.id}
/>
</div>
</div>
</Card>
))}
</div>
);
const hasActiveFilters = searchTerm || siteTypeFilter || hostingTypeFilter || statusFilter || integrationFilter;
if (loading) {
@@ -517,26 +637,11 @@ export default function SiteList() {
<PageHeader
title="Sites Management"
badge={{ icon: <GridIcon />, color: 'blue' }}
hideSiteSector={true}
/>
{/* Info Alert */}
<div className="mb-6">
<div className="flex items-center justify-between mb-4">
<p className="text-sm text-gray-600 dark:text-gray-400">
Manage your sites, configure industries, and select sectors. Multiple sites can be active simultaneously.
</p>
<div className="flex gap-2">
<Button onClick={() => navigate('/sites/builder')} variant="outline">
<PlusIcon className="w-4 h-4 mr-2" />
Create with Builder
</Button>
<Button onClick={handleCreateSite} variant="primary">
<PlusIcon className="w-4 h-4 mr-2" />
Add Site
</Button>
</div>
</div>
{/* Info Alert */}
<Alert
variant="info"
title="Sites Configuration"
@@ -544,233 +649,202 @@ export default function SiteList() {
/>
</div>
{/* Filters */}
<Card className="p-4 mb-6">
<div className="flex items-center gap-2 mb-4">
<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">
Filters
</h2>
{hasActiveFilters && (
<Button
variant="ghost"
size="sm"
onClick={clearFilters}
className="ml-auto"
{/* Custom Header Actions - Create buttons 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">
<PlusIcon className="w-4 h-4 mr-2" />
Create with Builder
</Button>
<Button onClick={handleCreateSite} variant="primary">
<PlusIcon className="w-4 h-4 mr-2" />
Add Site
</Button>
<div className="flex items-center gap-2">
<button
onClick={() => setViewType('table')}
className={`inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
viewType === 'table'
? 'bg-white text-gray-900 dark:bg-gray-800 dark:text-white shadow-sm border border-gray-200 dark:border-gray-700'
: 'text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
}`}
title="Table View"
>
Clear Filters
</Button>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
{/* Search */}
<div className="lg:col-span-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Search
</label>
<div className="relative">
<div className="absolute left-3 top-1/2 transform -translate-y-1/2">
<GridIcon className="w-4 h-4 text-gray-400" />
</div>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search sites..."
className="w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
/>
</div>
</div>
{/* Site Type */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Site Type
</label>
<SelectDropdown
options={SITE_TYPES}
value={siteTypeFilter}
onChange={(value) => setSiteTypeFilter(value)}
/>
</div>
{/* Hosting Type */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Hosting
</label>
<SelectDropdown
options={HOSTING_TYPES}
value={hostingTypeFilter}
onChange={(value) => setHostingTypeFilter(value)}
/>
</div>
{/* Status */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Status
</label>
<SelectDropdown
options={STATUS_OPTIONS}
value={statusFilter}
onChange={(value) => setStatusFilter(value)}
/>
<TableIcon className="w-4 h-4" />
<span className="hidden sm:inline">Table</span>
</button>
<button
onClick={() => setViewType('grid')}
className={`inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
viewType === 'grid'
? 'bg-white text-gray-900 dark:bg-gray-800 dark:text-white shadow-sm border border-gray-200 dark:border-gray-700'
: 'text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
}`}
title="Grid View"
>
<GridIcon className="w-4 h-4" />
<span className="hidden sm:inline">Grid</span>
</button>
</div>
</div>
<div className="mt-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Integrations
</label>
<SelectDropdown
options={INTEGRATION_OPTIONS}
value={integrationFilter}
onChange={(value) => setIntegrationFilter(value)}
/>
</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>
{/* Sites List */}
{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={handleCreateSite} variant="primary">
Create Your First Site
</Button>
)}
</Card>
{/* 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}
onEdit={(row) => handleEdit(row)}
onDelete={async (id) => {
await handleDeleteSite(id);
}}
getItemDisplayName={(row) => row.name}
/>
) : (
<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="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>
<h3 className="mb-3 text-lg font-semibold text-gray-800 dark:text-white/90">
{site.name}
</h3>
<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>
)}
<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">
{site.integration_count} integration{site.integration_count > 1 ? 's' : ''}
</Badge>
)}
</div>
{/* Status Text and Circle - Same row */}
<div className="absolute top-5 right-5 flex items-center gap-2">
<span className={`text-sm ${site.is_active ? 'text-green-600 dark:text-green-400 font-bold' : 'text-gray-400 dark:text-gray-500'} transition-colors duration-200`}>
{site.is_active ? 'Active' : 'Inactive'}
</span>
<div
className={`w-[25px] h-[25px] rounded-full ${site.is_active ? 'bg-green-500 dark:bg-green-600' : 'bg-gray-400 dark:bg-gray-500'} transition-colors duration-200`}
title={site.is_active ? 'Active site' : 'Inactive site'}
/>
</div>
<>
{/* 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 className="border-t border-gray-200 p-5 dark:border-gray-800">
{/* Quick Actions */}
<div className="grid grid-cols-3 gap-2 mb-3">
<Button
variant="outline"
size="sm"
onClick={() => handleView(site.id)}
className="w-full justify-center text-xs"
title="View Dashboard"
>
<EyeIcon className="w-3 h-3 mr-1" />
Dashboard
</Button>
<Button
variant="outline"
size="sm"
onClick={() => navigate(`/sites/${site.id}/content`)}
className="w-full justify-center text-xs"
title="View Content"
>
<FileIcon className="w-3 h-3 mr-1" />
Content
</Button>
<Button
variant="outline"
size="sm"
onClick={() => navigate(`/sites/${site.id}/pages`)}
className="w-full justify-center text-xs"
title="Manage Pages"
>
<PageIcon className="w-3 h-3 mr-1" />
Pages
</Button>
</div>
{/* Secondary Actions */}
<div className="flex items-center justify-between">
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => handleSettings(site.id)}
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"
>
<GridIcon className="w-4 h-4 mr-1" />
<span className="text-xs">Sectors</span>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => navigate(`/sites/${site.id}/settings`)}
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"
>
<PlugInIcon className="w-4 h-4 mr-1" />
<span className="text-xs">Settings</span>
</Button>
</div>
<Switch
checked={site.is_active}
onChange={(enabled) => handleToggle(site.id, enabled)}
disabled={togglingSiteId === site.id}
/>
</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>
</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 */}
{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={handleCreateSite} variant="primary">
Create Your First Site
</Button>
)}
</Card>
))}
</div>
) : (
renderGridView()
)}
</>
)}
{/* Create/Edit Site Modal */}
@@ -896,57 +970,6 @@ export default function SiteList() {
</div>
}
/>
{/* Site Details Modal - Editable */}
{selectedSite && (
<FormModal
isOpen={showDetailsModal}
onClose={() => {
setShowDetailsModal(false);
setSelectedSite(null);
}}
onSubmit={handleSaveDetails}
title={`Edit Site: ${selectedSite.name}`}
submitLabel="Save Changes"
fields={getSiteFormFields()}
isLoading={isSaving}
customFooter={
<div className="flex justify-between items-center pt-4 border-t border-gray-200 dark:border-gray-700">
<Button
variant="danger"
onClick={() => {
if (selectedSite) {
handleDeleteSite(selectedSite);
}
}}
disabled={isSaving}
>
Delete Site
</Button>
<div className="flex gap-3">
<Button
variant="outline"
onClick={() => {
setShowDetailsModal(false);
setSelectedSite(null);
}}
disabled={isSaving}
>
Cancel
</Button>
<Button
variant="primary"
onClick={handleSaveDetails}
disabled={isSaving}
>
{isSaving ? 'Saving...' : 'Save Changes'}
</Button>
</div>
</div>
}
/>
)}
</div>
);
}