Refactor Site Management Components and Update URL Parameters
- Changed `siteId` to `id` in `useParams` across multiple site-related components for consistency. - Removed "Sites" from the sidebar settings menu. - Updated navigation links in `SiteDashboard` to reflect new paths for integrations and deployments. - Enhanced `SiteList` component with improved site management features, including modals for site creation and sector configuration. - Added functionality to handle industry and sector selection for sites, with validation for maximum selections. - Improved UI elements and alerts for better user experience in site management.
This commit is contained in:
Binary file not shown.
@@ -210,7 +210,6 @@ const AppSidebar: React.FC = () => {
|
||||
subItems: [
|
||||
{ name: "General", path: "/settings" },
|
||||
{ name: "Plans", path: "/settings/plans" },
|
||||
{ name: "Sites", path: "/settings/sites" },
|
||||
{ name: "Integration", path: "/settings/integration" },
|
||||
{ name: "Publishing", path: "/settings/publishing" },
|
||||
{ name: "Import / Export", path: "/settings/import-export" },
|
||||
|
||||
@@ -28,7 +28,7 @@ interface ContentItem {
|
||||
}
|
||||
|
||||
export default function SiteContentManager() {
|
||||
const { siteId } = useParams<{ siteId: string }>();
|
||||
const { id: siteId } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const toast = useToast();
|
||||
const [content, setContent] = useState<ContentItem[]>([]);
|
||||
|
||||
@@ -46,7 +46,7 @@ interface SiteStats {
|
||||
}
|
||||
|
||||
export default function SiteDashboard() {
|
||||
const { siteId } = useParams<{ siteId: string }>();
|
||||
const { id: siteId } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const toast = useToast();
|
||||
const [site, setSite] = useState<Site | null>(null);
|
||||
@@ -152,14 +152,14 @@ export default function SiteDashboard() {
|
||||
value: stats?.integrations_count || 0,
|
||||
icon: <PlugIcon className="w-5 h-5" />,
|
||||
color: 'purple',
|
||||
link: `/sites/${siteId}/integrations`,
|
||||
link: `/sites/${siteId}/settings?tab=integrations`,
|
||||
},
|
||||
{
|
||||
label: 'Deployments',
|
||||
value: stats?.deployments_count || 0,
|
||||
icon: <TrendingUpIcon className="w-5 h-5" />,
|
||||
color: 'teal',
|
||||
link: `/sites/${siteId}/deployments`,
|
||||
link: `/sites/${siteId}/preview`,
|
||||
},
|
||||
{
|
||||
label: 'Total Content',
|
||||
@@ -256,7 +256,7 @@ export default function SiteDashboard() {
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => navigate(`/sites/${siteId}/integrations`)}
|
||||
onClick={() => navigate(`/sites/${siteId}/settings?tab=integrations`)}
|
||||
className="justify-start"
|
||||
>
|
||||
<PlugIcon className="w-4 h-4 mr-2" />
|
||||
@@ -264,11 +264,11 @@ export default function SiteDashboard() {
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => navigate(`/sites/${siteId}/deploy`)}
|
||||
onClick={() => navigate(`/sites/${siteId}/editor`)}
|
||||
className="justify-start"
|
||||
>
|
||||
<TrendingUpIcon className="w-4 h-4 mr-2" />
|
||||
Deploy Site
|
||||
<FileTextIcon className="w-4 h-4 mr-2" />
|
||||
Edit Site
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
61
frontend/src/pages/Sites/EXPECTED-PAGES-LIST.md
Normal file
61
frontend/src/pages/Sites/EXPECTED-PAGES-LIST.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Expected Frontend Pages - Sites Module
|
||||
|
||||
## Currently Available Pages (4)
|
||||
|
||||
1. **Site List** (`/sites`) - Basic list with filters
|
||||
2. **Site Builder** (`/sites/builder`) - Placeholder only
|
||||
3. **Blueprints** (`/sites/blueprints`) - Placeholder only
|
||||
4. **Sites Management** (`/settings/sites`) - Should be merged into Site List
|
||||
|
||||
## Expected Pages (16 Total)
|
||||
|
||||
### Site Management Pages (12)
|
||||
|
||||
| Page | Route | Status | Description |
|
||||
|------|-------|--------|-------------|
|
||||
| **All Sites** | `/sites` | ✅ Exists | Site list with filters, search, type badges |
|
||||
| **Site Dashboard** | `/sites/:id` | ✅ Exists | Individual site dashboard with stats |
|
||||
| **Site Content** | `/sites/:id/content` | ✅ Exists | Content list for a site |
|
||||
| **Site Editor** | `/sites/:id/editor` | ✅ Exists | Site content editor |
|
||||
| **Page Manager** | `/sites/:id/pages` | ✅ Exists | Manage pages for a site |
|
||||
| **New Page** | `/sites/:id/pages/new` | ✅ Exists | Create new page |
|
||||
| **Edit Page** | `/sites/:id/pages/:pageId/edit` | ✅ Exists | Edit existing page |
|
||||
| **Post Editor** | `/sites/:id/posts/:postId` | ✅ Exists | View/edit post |
|
||||
| **Edit Post** | `/sites/:id/posts/:postId/edit` | ✅ Exists | Edit post |
|
||||
| **Site Preview** | `/sites/:id/preview` | ✅ Exists | Preview site |
|
||||
| **Site Settings** | `/sites/:id/settings` | ✅ Exists | Site settings (General, SEO, OG, Schema, Integrations) |
|
||||
| **Site Management** | `/sites/manage` | ✅ Exists | Site management dashboard |
|
||||
|
||||
### Site Builder Pages (3)
|
||||
|
||||
| Page | Route | Status | Description |
|
||||
|------|-------|--------|-------------|
|
||||
| **Site Builder Wizard** | `/sites/builder` | ⚠️ Placeholder | AI-powered site creation wizard (needs migration) |
|
||||
| **Site Builder Preview** | `/sites/builder/preview` | ⚠️ Placeholder | Preview site during building (needs migration) |
|
||||
| **Blueprints** | `/sites/blueprints` | ⚠️ Placeholder | View and manage all site blueprints (needs migration) |
|
||||
|
||||
### Settings Pages (1)
|
||||
|
||||
| Page | Route | Status | Description |
|
||||
|------|-------|--------|-------------|
|
||||
| **Sites Settings** | `/settings/sites` | ❌ Should be removed | Site configuration (should be merged into Site List) |
|
||||
|
||||
## Issues to Fix
|
||||
|
||||
1. **Sites in Settings dropdown** - Broken UX, should be removed
|
||||
2. **Site management features** - Should be merged into Site List page
|
||||
3. **Wizard is placeholder** - Needs full migration from site-builder container
|
||||
4. **Pages incomplete** - Many pages exist but lack full functionality
|
||||
|
||||
## Required Features for Site List
|
||||
|
||||
- Create new site (modal)
|
||||
- Edit site (modal)
|
||||
- Delete site (with confirmation)
|
||||
- Toggle site active/inactive
|
||||
- Configure sectors for site (modal)
|
||||
- View site details
|
||||
- Site cards with all information
|
||||
- Industry/sector badges
|
||||
- Status indicators
|
||||
|
||||
@@ -30,7 +30,7 @@ interface SiteBlueprint {
|
||||
}
|
||||
|
||||
export default function SiteContentEditor() {
|
||||
const { siteId } = useParams<{ siteId: string }>();
|
||||
const { id: siteId } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const toast = useToast();
|
||||
const [blueprints, setBlueprints] = useState<SiteBlueprint[]>([]);
|
||||
|
||||
92
frontend/src/pages/Sites/FIXES-SUMMARY.md
Normal file
92
frontend/src/pages/Sites/FIXES-SUMMARY.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# Sites Module Fixes Summary
|
||||
|
||||
## ✅ Completed Fixes
|
||||
|
||||
### 1. Removed Sites from Settings Dropdown
|
||||
- **File**: `frontend/src/layout/AppSidebar.tsx`
|
||||
- **Change**: Removed "Sites" from Settings submenu
|
||||
- **Result**: Sites now only accessible from main "Sites" menu in WORKFLOWS section
|
||||
|
||||
### 2. Merged Site Management into Site List Page
|
||||
- **File**: `frontend/src/pages/Sites/List.tsx`
|
||||
- **Changes**:
|
||||
- Added all site management functionality from Settings/Sites.tsx
|
||||
- Added create/edit site modals
|
||||
- Added sectors configuration modal
|
||||
- Added site details/edit modal with delete option
|
||||
- Added toggle switch for active/inactive status
|
||||
- Updated site cards to match Settings/Sites.tsx style
|
||||
- Added industry/sector badges
|
||||
- Added site type badges
|
||||
- Added integration count badges
|
||||
- Added "Create with Builder" button alongside "Add Site"
|
||||
- Added info alert about site configuration
|
||||
|
||||
### 3. Updated Site Cards
|
||||
- **Features Added**:
|
||||
- Site icon
|
||||
- Site description display
|
||||
- Domain display
|
||||
- Industry name badge
|
||||
- Sector count badge (X / 5 Sectors)
|
||||
- Integration count badge
|
||||
- Site type badge (Site Builder / WordPress)
|
||||
- Active/Inactive status indicator with colored circle
|
||||
- Settings button (opens sectors modal)
|
||||
- Details button (opens edit modal)
|
||||
- Toggle switch for active/inactive
|
||||
|
||||
## ⚠️ Still Needs Work
|
||||
|
||||
### 1. Site Builder Wizard Migration
|
||||
- **Current Status**: Placeholder only (`/sites/builder`)
|
||||
- **Needs**: Full migration from `site-builder` container
|
||||
- **Files to Migrate**:
|
||||
- `site-builder/src/pages/wizard/WizardPage.tsx`
|
||||
- `site-builder/src/pages/wizard/steps/*.tsx` (4 step components)
|
||||
- `site-builder/src/state/builderStore.ts`
|
||||
- `site-builder/src/api/builder.api.ts`
|
||||
- `site-builder/src/types/siteBuilder.ts`
|
||||
- **Target Location**: `frontend/src/pages/Sites/Builder/Wizard.tsx`
|
||||
|
||||
### 2. Blueprints Page
|
||||
- **Current Status**: Placeholder only (`/sites/blueprints`)
|
||||
- **Needs**: Full implementation to list and manage site blueprints
|
||||
|
||||
### 3. Site Builder Preview
|
||||
- **Current Status**: Placeholder only (`/sites/builder/preview`)
|
||||
- **Needs**: Full preview canvas implementation
|
||||
|
||||
### 4. Other Site Pages
|
||||
- Many pages exist but may need complete functionality:
|
||||
- Site Dashboard (`/sites/:id`)
|
||||
- Site Content (`/sites/:id/content`)
|
||||
- Site Editor (`/sites/:id/editor`)
|
||||
- Page Manager (`/sites/:id/pages`)
|
||||
- Post Editor (`/sites/:id/posts/:postId`)
|
||||
- Site Preview (`/sites/:id/preview`)
|
||||
- Site Settings (`/sites/:id/settings`)
|
||||
|
||||
## 📋 Expected Pages List
|
||||
|
||||
See `EXPECTED-PAGES-LIST.md` for complete list of all expected frontend pages.
|
||||
|
||||
## 🎯 Next Steps
|
||||
|
||||
1. **Priority 1**: Migrate Site Builder Wizard
|
||||
- Copy wizard components from site-builder container
|
||||
- Adapt to main app structure
|
||||
- Ensure API calls work correctly
|
||||
- Test full wizard flow
|
||||
|
||||
2. **Priority 2**: Implement Blueprints Page
|
||||
- List all site blueprints
|
||||
- Show blueprint status
|
||||
- Allow editing/deleting blueprints
|
||||
- Link to wizard for creating new blueprints
|
||||
|
||||
3. **Priority 3**: Verify Other Pages
|
||||
- Check each page has complete functionality
|
||||
- Add missing features
|
||||
- Ensure proper navigation between pages
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { PlusIcon, EditIcon, SettingsIcon, EyeIcon, TrashIcon, FilterIcon, SearchIcon, PlugIcon } from 'lucide-react';
|
||||
import { PlusIcon, EditIcon, SettingsIcon, EyeIcon, TrashIcon, FilterIcon, SearchIcon, PlugIcon, FileTextIcon } from 'lucide-react';
|
||||
import PageMeta from '../../components/common/PageMeta';
|
||||
import { Card } from '../../components/ui/card';
|
||||
import Button from '../../components/ui/button/Button';
|
||||
@@ -14,20 +14,30 @@ 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 {
|
||||
fetchSites,
|
||||
createSite,
|
||||
updateSite,
|
||||
deleteSite,
|
||||
setActiveSite,
|
||||
selectSectorsForSite,
|
||||
fetchIndustries,
|
||||
fetchSiteSectors,
|
||||
Site as SiteType,
|
||||
Industry,
|
||||
} from '../../services/api';
|
||||
|
||||
interface Site {
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
site_type: string;
|
||||
hosting_type: string;
|
||||
status: string;
|
||||
is_active: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
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;
|
||||
}
|
||||
|
||||
export default function SiteList() {
|
||||
@@ -37,6 +47,26 @@ export default function SiteList() {
|
||||
const [filteredSites, setFilteredSites] = useState<Site[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// 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[]>([]);
|
||||
const [selectedIndustry, setSelectedIndustry] = useState<string>('');
|
||||
const [selectedSectors, setSelectedSectors] = useState<string[]>([]);
|
||||
const [isSelectingSectors, setIsSelectingSectors] = useState(false);
|
||||
|
||||
// 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('');
|
||||
@@ -46,6 +76,7 @@ export default function SiteList() {
|
||||
|
||||
useEffect(() => {
|
||||
loadSites();
|
||||
loadIndustries();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -55,8 +86,9 @@ export default function SiteList() {
|
||||
const loadSites = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await fetchAPI('/v1/auth/sites/');
|
||||
if (data && Array.isArray(data)) {
|
||||
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) => {
|
||||
@@ -83,6 +115,15 @@ export default function SiteList() {
|
||||
}
|
||||
};
|
||||
|
||||
const loadIndustries = async () => {
|
||||
try {
|
||||
const response = await fetchIndustries();
|
||||
setIndustries(response.industries || []);
|
||||
} catch (error: any) {
|
||||
console.error('Failed to load industries:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const applyFilters = () => {
|
||||
let filtered = [...sites];
|
||||
|
||||
@@ -127,7 +168,14 @@ export default function SiteList() {
|
||||
};
|
||||
|
||||
const handleCreateSite = () => {
|
||||
navigate('/sites/builder');
|
||||
setSelectedSite(null);
|
||||
setFormData({
|
||||
name: '',
|
||||
domain: '',
|
||||
description: '',
|
||||
is_active: true,
|
||||
});
|
||||
setShowSiteModal(true);
|
||||
};
|
||||
|
||||
const handleIntegration = (siteId: number) => {
|
||||
@@ -135,11 +183,250 @@ export default function SiteList() {
|
||||
};
|
||||
|
||||
const handleEdit = (siteId: number) => {
|
||||
navigate(`/sites/${siteId}/edit`);
|
||||
const site = sites.find(s => s.id === siteId);
|
||||
if (site) {
|
||||
handleEditSite(site);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditSite = (site: Site) => {
|
||||
setSelectedSite(site);
|
||||
setFormData({
|
||||
name: site.name || '',
|
||||
domain: site.domain || '',
|
||||
description: site.description || '',
|
||||
is_active: site.is_active || false,
|
||||
});
|
||||
setShowSiteModal(true);
|
||||
};
|
||||
|
||||
const handleSettings = (siteId: number) => {
|
||||
navigate(`/sites/${siteId}/settings`);
|
||||
const site = sites.find(s => s.id === siteId);
|
||||
if (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 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);
|
||||
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 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 (site: Site) => {
|
||||
if (!window.confirm(`Are you sure you want to delete "${site.name}"? This action cannot be undone.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteSite(site.id);
|
||||
toast.success('Site deleted successfully');
|
||||
await loadSites();
|
||||
if (showDetailsModal) {
|
||||
setShowDetailsModal(false);
|
||||
setSelectedSite(null);
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to delete site: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
return industry?.sectors || [];
|
||||
};
|
||||
|
||||
const handleView = (siteId: number) => {
|
||||
@@ -215,21 +502,40 @@ export default function SiteList() {
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<PageMeta title="Site List - IGNY8" />
|
||||
<PageMeta title="Sites Management - IGNY8" />
|
||||
|
||||
<div className="mb-6 flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
Site List
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
View and manage all your sites with advanced filtering
|
||||
</p>
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
Sites Management
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
||||
Manage your sites, configure industries, and select sectors. Multiple sites can be active simultaneously.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={() => navigate('/sites/manage')} variant="outline">
|
||||
<SettingsIcon className="w-4 h-4 mr-2" />
|
||||
Site Management
|
||||
</Button>
|
||||
<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>
|
||||
<Button onClick={handleCreateSite} variant="primary">
|
||||
<PlusIcon className="w-4 h-4 mr-2" />
|
||||
Create New Site
|
||||
</Button>
|
||||
|
||||
{/* Info Alert */}
|
||||
<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>
|
||||
|
||||
{/* Filters */}
|
||||
@@ -341,88 +647,297 @@ export default function SiteList() {
|
||||
)}
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 xl:grid-cols-3">
|
||||
{filteredSites.map((site) => (
|
||||
<Card key={site.id} className="p-4 hover:shadow-lg transition-shadow">
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{site.name}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{site.slug}
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
className={`px-2 py-1 text-xs rounded ${
|
||||
site.is_active
|
||||
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
|
||||
: 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200'
|
||||
}`}
|
||||
>
|
||||
{site.is_active ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
<Card key={site.id} className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/3 hover:shadow-lg transition-shadow">
|
||||
<div className="relative p-5 pb-9">
|
||||
<div className="mb-5 inline-flex h-10 w-10 items-center justify-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40" fill="none">
|
||||
<rect width="40" height="40" rx="8" fill="#3B82F6"/>
|
||||
<path d="M12 16L20 10L28 16V28C28 28.5304 27.7893 29.0391 27.4142 29.4142C27.0391 29.7893 26.5304 30 26 30H14C13.4696 30 12.9609 29.7893 12.5858 29.4142C12.2107 29.0391 12 28.5304 12 28V16Z" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
<path d="M16 30V20H24V30" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 text-xs">
|
||||
<span className="px-2 py-1 bg-blue-100 text-blue-800 dark:bg-blue-900 dark:bg-blue-200 rounded capitalize">
|
||||
{site.site_type}
|
||||
</span>
|
||||
<span className="px-2 py-1 bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200 rounded capitalize">
|
||||
{site.hosting_type}
|
||||
</span>
|
||||
<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 && (
|
||||
<span className="px-2 py-1 bg-teal-100 text-teal-800 dark:bg-teal-900 dark:text-teal-200 rounded">
|
||||
<Badge variant="soft" color="success" size="xs">
|
||||
{site.integration_count} integration{site.integration_count > 1 ? 's' : ''}
|
||||
</span>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-3 border-t border-gray-200 dark:border-gray-700">
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400">
|
||||
{site.page_count || 0} pages
|
||||
</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>
|
||||
</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"
|
||||
>
|
||||
<FileTextIcon 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"
|
||||
>
|
||||
<FileTextIcon 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="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleView(site.id)}
|
||||
title="View"
|
||||
>
|
||||
<EyeIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEdit(site.id)}
|
||||
title="Edit"
|
||||
>
|
||||
<EditIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleSettings(site.id)}
|
||||
title="Settings"
|
||||
className="shadow-theme-xs inline-flex h-9 w-9 items-center justify-center rounded-lg border border-gray-300 text-gray-700 dark:border-gray-700 dark:text-gray-400"
|
||||
title="Configure Sectors"
|
||||
>
|
||||
<SettingsIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(site.id)}
|
||||
title="Delete"
|
||||
onClick={() => navigate(`/sites/${site.id}/settings`)}
|
||||
className="shadow-theme-xs inline-flex h-9 w-9 items-center justify-center rounded-lg border border-gray-300 text-gray-700 dark:border-gray-700 dark:text-gray-400"
|
||||
title="Site Settings"
|
||||
>
|
||||
<TrashIcon className="w-4 h-4" />
|
||||
<SettingsIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<Switch
|
||||
checked={site.is_active}
|
||||
onChange={(enabled) => handleToggle(site.id, enabled)}
|
||||
disabled={togglingSiteId === site.id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 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}
|
||||
/>
|
||||
|
||||
{/* Sectors Selection Modal */}
|
||||
<FormModal
|
||||
isOpen={showSectorsModal}
|
||||
onClose={() => setShowSectorsModal(false)}
|
||||
onSubmit={handleSelectSectors}
|
||||
title={selectedSite ? `Configure Sectors for ${selectedSite.name}` : 'Configure Sectors'}
|
||||
submitLabel={isSelectingSectors ? 'Saving...' : 'Save Sectors'}
|
||||
cancelLabel="Cancel"
|
||||
isLoading={isSelectingSectors}
|
||||
className="max-w-2xl"
|
||||
customBody={
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Select Industry
|
||||
</label>
|
||||
<select
|
||||
value={selectedIndustry}
|
||||
onChange={(e) => {
|
||||
setSelectedIndustry(e.target.value);
|
||||
setSelectedSectors([]);
|
||||
}}
|
||||
className="h-9 w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
>
|
||||
<option value="">Select an industry...</option>
|
||||
{industries.map((industry) => (
|
||||
<option key={industry.slug} value={industry.slug}>
|
||||
{industry.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{selectedIndustry && (
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{industries.find(i => i.slug === selectedIndustry)?.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedIndustry && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Select Sectors (max 5)
|
||||
</label>
|
||||
<div className="space-y-2 max-h-64 overflow-y-auto border border-gray-200 rounded-lg p-4 dark:border-gray-700">
|
||||
{getIndustrySectors().map((sector) => (
|
||||
<label
|
||||
key={sector.slug}
|
||||
className="flex items-start space-x-3 p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedSectors.includes(sector.slug)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
if (selectedSectors.length >= 5) {
|
||||
toast.error('Maximum 5 sectors allowed per site');
|
||||
return;
|
||||
}
|
||||
setSelectedSectors([...selectedSectors, sector.slug]);
|
||||
} else {
|
||||
setSelectedSectors(selectedSectors.filter(s => s !== sector.slug));
|
||||
}
|
||||
}}
|
||||
className="mt-1 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm text-gray-900 dark:text-white">
|
||||
{sector.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{sector.description}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
Selected: {selectedSectors.length} / 5 sectors
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
customFooter={
|
||||
<div className="flex justify-end gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setShowSectorsModal(false)}
|
||||
disabled={isSelectingSectors}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
disabled={!selectedIndustry || selectedSectors.length === 0 || isSelectingSectors}
|
||||
>
|
||||
{isSelectingSectors ? 'Saving...' : 'Save Sectors'}
|
||||
</Button>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -167,15 +167,28 @@ export default function SiteManagement() {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 text-xs">
|
||||
<span className="px-2 py-1 bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 rounded capitalize">
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
<SiteTypeBadge hostingType={site.hosting_type} />
|
||||
<Badge variant="soft" color="neutral" size="xs">
|
||||
{site.site_type}
|
||||
</span>
|
||||
<span className="px-2 py-1 bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200 rounded capitalize">
|
||||
{site.hosting_type}
|
||||
</span>
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* WordPress Integration Button */}
|
||||
{site.hosting_type === 'wordpress' && (
|
||||
<div className="pt-2">
|
||||
<Button
|
||||
variant={site.has_wordpress_integration ? "outline" : "primary"}
|
||||
size="sm"
|
||||
onClick={() => handleIntegration(site.id)}
|
||||
className="w-full"
|
||||
>
|
||||
<PlugIcon className="w-4 h-4 mr-2" />
|
||||
{site.has_wordpress_integration ? 'Manage Integration' : 'Connect WordPress'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between pt-3 border-t border-gray-200 dark:border-gray-700">
|
||||
<div className="text-xs text-gray-600 dark:text-gray-400">
|
||||
{site.page_count || 0} pages
|
||||
|
||||
@@ -98,7 +98,7 @@ const DraggablePageItem: React.FC<{
|
||||
};
|
||||
|
||||
export default function PageManager() {
|
||||
const { siteId } = useParams<{ siteId: string }>();
|
||||
const { id: siteId } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const toast = useToast();
|
||||
const [pages, setPages] = useState<Page[]>([]);
|
||||
|
||||
@@ -35,7 +35,7 @@ interface Content {
|
||||
}
|
||||
|
||||
export default function PostEditor() {
|
||||
const { siteId, postId } = useParams<{ siteId: string; postId?: string }>();
|
||||
const { id: siteId, postId } = useParams<{ id: string; postId?: string }>();
|
||||
const navigate = useNavigate();
|
||||
const toast = useToast();
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||
import { fetchAPI } from '../../services/api';
|
||||
|
||||
export default function SitePreview() {
|
||||
const { siteId } = useParams<{ siteId: string }>();
|
||||
const { id: siteId } = useParams<{ id: string }>();
|
||||
const toast = useToast();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||
|
||||
@@ -20,7 +20,7 @@ import WordPressIntegrationModal, { WordPressIntegrationFormData } from '../../c
|
||||
import { integrationApi, SiteIntegration } from '../../services/integration.api';
|
||||
|
||||
export default function SiteSettings() {
|
||||
const { siteId } = useParams<{ siteId: string }>();
|
||||
const { id: siteId } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const toast = useToast();
|
||||
|
||||
Reference in New Issue
Block a user