1
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user