This commit is contained in:
alorig
2025-11-18 05:21:27 +05:00
parent a0f3e3a778
commit 9a6d47b91b
34 changed files with 3258 additions and 9 deletions

View File

@@ -5,6 +5,7 @@ import ValidationCard from '../../components/common/ValidationCard';
import ImageGenerationCard from '../../components/common/ImageGenerationCard';
import ImageResultCard from '../../components/common/ImageResultCard';
import ImageServiceCard from '../../components/common/ImageServiceCard';
import SiteIntegrationsSection from '../../components/integration/SiteIntegrationsSection';
import { Modal } from '../../components/ui/modal';
import FormModal, { FormField } from '../../components/common/FormModal';
import Button from '../../components/ui/button/Button';
@@ -1082,6 +1083,9 @@ export default function Integration() {
}
/>
</div>
{/* Site Integrations Section */}
<SiteIntegrationsSection />
</div>
{/* Details Modal */}

View File

@@ -0,0 +1,165 @@
/**
* Publishing Settings Page
* Phase 6: Site Integration & Multi-Destination Publishing
*/
import React, { useState, useEffect } from 'react';
import PageMeta from '../../components/common/PageMeta';
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
import Checkbox from '../../components/form/input/Checkbox';
import Label from '../../components/form/Label';
import PublishingRules, { PublishingRule } from '../../components/publishing/PublishingRules';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchAPI } from '../../services/api';
export default function Publishing() {
const toast = useToast();
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [defaultDestinations, setDefaultDestinations] = useState<string[]>(['sites']);
const [autoPublishEnabled, setAutoPublishEnabled] = useState(false);
const [publishingRules, setPublishingRules] = useState<PublishingRule[]>([]);
useEffect(() => {
loadSettings();
}, []);
const loadSettings = async () => {
try {
setLoading(true);
// TODO: Load from backend API when endpoint is available
// For now, use defaults
setDefaultDestinations(['sites']);
setAutoPublishEnabled(false);
setPublishingRules([]);
} catch (error: any) {
toast.error(`Failed to load settings: ${error.message}`);
} finally {
setLoading(false);
}
};
const handleSave = async () => {
try {
setSaving(true);
// TODO: Save to backend API when endpoint is available
toast.success('Publishing settings saved successfully');
} catch (error: any) {
toast.error(`Failed to save settings: ${error.message}`);
} finally {
setSaving(false);
}
};
const handleToggleDestination = (destination: string) => {
setDefaultDestinations((prev) =>
prev.includes(destination)
? prev.filter((d) => d !== destination)
: [...prev, destination]
);
};
const DESTINATIONS = [
{ value: 'sites', label: 'IGNY8 Sites' },
{ value: 'wordpress', label: 'WordPress' },
{ value: 'shopify', label: 'Shopify' },
];
if (loading) {
return (
<div className="p-6">
<PageMeta title="Publishing Settings" />
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading...</div>
</div>
</div>
);
}
return (
<div className="p-6">
<PageMeta title="Publishing Settings - IGNY8" />
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Publishing Settings
</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Configure default publishing destinations and rules
</p>
</div>
<div className="space-y-6">
{/* Default Destinations */}
<Card className="p-6">
<div className="space-y-4">
<div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-1">
Default Publishing Destinations
</h2>
<p className="text-sm text-gray-600 dark:text-gray-400">
Select default platforms where content will be published
</p>
</div>
<div className="space-y-2">
{DESTINATIONS.map((dest) => (
<div key={dest.value} className="flex items-center gap-3">
<Checkbox
checked={defaultDestinations.includes(dest.value)}
onChange={() => handleToggleDestination(dest.value)}
label={dest.label}
/>
</div>
))}
</div>
</div>
</Card>
{/* Auto-Publish Settings */}
<Card className="p-6">
<div className="space-y-4">
<div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-1">
Auto-Publish Settings
</h2>
<p className="text-sm text-gray-600 dark:text-gray-400">
Automatically publish content when it's ready
</p>
</div>
<div className="flex items-center gap-3">
<Checkbox
checked={autoPublishEnabled}
onChange={(e) => setAutoPublishEnabled(e.target.checked)}
label="Enable auto-publish"
/>
</div>
{autoPublishEnabled && (
<div className="mt-4 p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
<p className="text-sm text-blue-800 dark:text-blue-200">
When enabled, content will be automatically published to selected destinations
when generation is complete.
</p>
</div>
)}
</div>
</Card>
{/* Publishing Rules */}
<Card className="p-6">
<PublishingRules rules={publishingRules} onChange={setPublishingRules} />
</Card>
{/* Save Button */}
<div className="flex justify-end">
<Button onClick={handleSave} variant="primary" disabled={saving}>
{saving ? 'Saving...' : 'Save Settings'}
</Button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,83 @@
/**
* Site Content Editor
* Phase 6: Site Integration & Multi-Destination Publishing
*/
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import PageMeta from '../../components/common/PageMeta';
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchAPI } from '../../services/api';
interface Page {
id: number;
slug: string;
title: string;
type: string;
status: string;
blocks: any[];
}
export default function SiteContentEditor() {
const { siteId } = useParams<{ siteId: string }>();
const navigate = useNavigate();
const toast = useToast();
const [pages, setPages] = useState<Page[]>([]);
const [loading, setLoading] = useState(true);
const [selectedPage, setSelectedPage] = useState<Page | null>(null);
useEffect(() => {
if (siteId) {
loadPages();
}
}, [siteId]);
const loadPages = async () => {
try {
setLoading(true);
// TODO: Load pages from SiteBlueprint API
// For now, placeholder
setPages([]);
} catch (error: any) {
toast.error(`Failed to load pages: ${error.message}`);
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div className="p-6">
<PageMeta title="Site Content Editor" />
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading pages...</div>
</div>
</div>
);
}
return (
<div className="p-6">
<PageMeta title="Site Content Editor - IGNY8" />
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Site Content Editor
</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Edit content for site pages
</p>
</div>
<Card className="p-6">
<div className="text-center py-12">
<p className="text-gray-600 dark:text-gray-400">
Content editor will be implemented in Phase 7
</p>
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,202 @@
/**
* Site Management Dashboard
* Phase 6: Site Integration & Multi-Destination Publishing
*/
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { PlusIcon, EditIcon, SettingsIcon, EyeIcon, TrashIcon } from 'lucide-react';
import PageMeta from '../../components/common/PageMeta';
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchAPI } from '../../services/api';
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;
page_count?: number;
integration_count?: number;
}
export default function SiteManagement() {
const navigate = useNavigate();
const toast = useToast();
const [sites, setSites] = useState<Site[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadSites();
}, []);
const loadSites = async () => {
try {
setLoading(true);
const data = await fetchAPI('/v1/auth/sites/');
if (data && Array.isArray(data)) {
setSites(data);
}
} catch (error: any) {
toast.error(`Failed to load sites: ${error.message}`);
} finally {
setLoading(false);
}
};
const handleCreateSite = () => {
navigate('/site-builder');
};
const handleEdit = (siteId: number) => {
navigate(`/sites/${siteId}/edit`);
};
const handleSettings = (siteId: number) => {
navigate(`/sites/${siteId}/settings`);
};
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}`);
}
};
if (loading) {
return (
<div className="p-6">
<PageMeta title="Site Management" />
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading sites...</div>
</div>
</div>
);
}
return (
<div className="p-6">
<PageMeta title="Site 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 Management
</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Manage your sites, pages, and content
</p>
</div>
<Button onClick={handleCreateSite} variant="primary">
<PlusIcon className="w-4 h-4 mr-2" />
Create New Site
</Button>
</div>
{sites.length === 0 ? (
<Card className="p-12 text-center">
<p className="text-gray-600 dark:text-gray-400 mb-4">
No sites created yet
</p>
<Button onClick={handleCreateSite} variant="primary">
Create Your First Site
</Button>
</Card>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{sites.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>
</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">
{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>
</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>
<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"
size="sm"
onClick={() => handleSettings(site.id)}
title="Settings"
>
<SettingsIcon className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDelete(site.id)}
title="Delete"
>
<TrashIcon className="w-4 h-4" />
</Button>
</div>
</div>
</div>
</Card>
))}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,170 @@
/**
* Page Manager
* Phase 6: Site Integration & Multi-Destination Publishing
*/
import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { PlusIcon, EditIcon, TrashIcon, ArrowUpIcon, ArrowDownIcon } from 'lucide-react';
import PageMeta from '../../components/common/PageMeta';
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchAPI } from '../../services/api';
interface Page {
id: number;
slug: string;
title: string;
type: string;
status: string;
order: number;
blocks: any[];
}
export default function PageManager() {
const { siteId } = useParams<{ siteId: string }>();
const toast = useToast();
const [pages, setPages] = useState<Page[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (siteId) {
loadPages();
}
}, [siteId]);
const loadPages = async () => {
try {
setLoading(true);
// TODO: Load pages from SiteBlueprint API
// For now, placeholder
setPages([]);
} catch (error: any) {
toast.error(`Failed to load pages: ${error.message}`);
} finally {
setLoading(false);
}
};
const handleAddPage = () => {
// TODO: Navigate to page creation
toast.info('Page creation will be implemented in Phase 7');
};
const handleEditPage = (pageId: number) => {
// TODO: Navigate to page editor
toast.info('Page editor will be implemented in Phase 7');
};
const handleDeletePage = async (pageId: number) => {
if (!confirm('Are you sure you want to delete this page?')) return;
// TODO: Delete page
toast.info('Page deletion will be implemented in Phase 7');
};
const handleMovePage = async (pageId: number, direction: 'up' | 'down') => {
// TODO: Update page order
toast.info('Page reordering will be implemented in Phase 7');
};
if (loading) {
return (
<div className="p-6">
<PageMeta title="Page Manager" />
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading pages...</div>
</div>
</div>
);
}
return (
<div className="p-6">
<PageMeta title="Page Manager - IGNY8" />
<div className="mb-6 flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Page Manager
</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Manage pages for your site
</p>
</div>
<Button onClick={handleAddPage} variant="primary">
<PlusIcon className="w-4 h-4 mr-2" />
Add Page
</Button>
</div>
{pages.length === 0 ? (
<Card className="p-12 text-center">
<p className="text-gray-600 dark:text-gray-400 mb-4">
No pages created yet
</p>
<Button onClick={handleAddPage} variant="primary">
Add Your First Page
</Button>
</Card>
) : (
<Card className="p-6">
<div className="space-y-3">
{pages.map((page, index) => (
<div
key={page.id}
className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-700 rounded-lg"
>
<div className="flex items-center gap-4 flex-1">
<div className="flex flex-col gap-1">
<Button
variant="ghost"
size="sm"
onClick={() => handleMovePage(page.id, 'up')}
disabled={index === 0}
>
<ArrowUpIcon className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleMovePage(page.id, 'down')}
disabled={index === pages.length - 1}
>
<ArrowDownIcon className="w-4 h-4" />
</Button>
</div>
<div className="flex-1">
<h3 className="font-semibold text-gray-900 dark:text-white">
{page.title}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
/{page.slug} {page.type} {page.status}
</p>
</div>
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => handleEditPage(page.id)}
>
<EditIcon className="w-4 h-4 mr-1" />
Edit
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDeletePage(page.id)}
>
<TrashIcon className="w-4 h-4" />
</Button>
</div>
</div>
))}
</div>
</Card>
)}
</div>
);
}

View File

@@ -0,0 +1,174 @@
/**
* Site Settings
* Phase 6: Site Integration & Multi-Destination Publishing
*/
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import PageMeta from '../../components/common/PageMeta';
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
import Label from '../../components/form/Label';
import SelectDropdown from '../../components/form/SelectDropdown';
import Checkbox from '../../components/form/input/Checkbox';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchAPI } from '../../services/api';
export default function SiteSettings() {
const { siteId } = useParams<{ siteId: string }>();
const navigate = useNavigate();
const toast = useToast();
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [site, setSite] = useState<any>(null);
const [formData, setFormData] = useState({
name: '',
slug: '',
site_type: 'marketing',
hosting_type: 'igny8_sites',
is_active: true,
});
useEffect(() => {
if (siteId) {
loadSite();
}
}, [siteId]);
const loadSite = async () => {
try {
setLoading(true);
const data = await fetchAPI(`/v1/auth/sites/${siteId}/`);
if (data) {
setSite(data);
setFormData({
name: data.name || '',
slug: data.slug || '',
site_type: data.site_type || 'marketing',
hosting_type: data.hosting_type || 'igny8_sites',
is_active: data.is_active !== false,
});
}
} catch (error: any) {
toast.error(`Failed to load site: ${error.message}`);
} finally {
setLoading(false);
}
};
const handleSave = async () => {
try {
setSaving(true);
await fetchAPI(`/v1/auth/sites/${siteId}/`, {
method: 'PUT',
body: JSON.stringify(formData),
});
toast.success('Site settings saved successfully');
loadSite();
} catch (error: any) {
toast.error(`Failed to save settings: ${error.message}`);
} finally {
setSaving(false);
}
};
const SITE_TYPES = [
{ value: 'marketing', label: 'Marketing Site' },
{ value: 'ecommerce', label: 'Ecommerce Site' },
{ value: 'blog', label: 'Blog' },
{ value: 'portfolio', label: 'Portfolio' },
{ value: 'corporate', label: 'Corporate' },
];
const HOSTING_TYPES = [
{ value: 'igny8_sites', label: 'IGNY8 Sites' },
{ value: 'wordpress', label: 'WordPress' },
{ value: 'shopify', label: 'Shopify' },
{ value: 'multi', label: 'Multi-Destination' },
];
if (loading) {
return (
<div className="p-6">
<PageMeta title="Site Settings" />
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading site settings...</div>
</div>
</div>
);
}
return (
<div className="p-6">
<PageMeta title="Site Settings - IGNY8" />
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Site Settings
</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Configure site type, hosting, and other settings
</p>
</div>
<div className="space-y-6">
<Card className="p-6">
<div className="space-y-4">
<div>
<Label>Site Name</Label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="mt-1 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>Slug</Label>
<input
type="text"
value={formData.slug}
onChange={(e) => setFormData({ ...formData, slug: e.target.value })}
className="mt-1 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>Site Type</Label>
<SelectDropdown
options={SITE_TYPES}
value={formData.site_type}
onChange={(e) => setFormData({ ...formData, site_type: e.target.value })}
/>
</div>
<div>
<Label>Hosting Type</Label>
<SelectDropdown
options={HOSTING_TYPES}
value={formData.hosting_type}
onChange={(e) => setFormData({ ...formData, hosting_type: e.target.value })}
/>
</div>
<div>
<Checkbox
checked={formData.is_active}
onChange={(e) => setFormData({ ...formData, is_active: e.target.checked })}
label="Active"
/>
</div>
</div>
</Card>
<div className="flex justify-end">
<Button onClick={handleSave} variant="primary" disabled={saving}>
{saving ? 'Saving...' : 'Save Settings'}
</Button>
</div>
</div>
</div>
);
}

View File

@@ -47,6 +47,7 @@ export default function Tasks() {
const [clusterFilter, setClusterFilter] = useState('');
const [structureFilter, setStructureFilter] = useState('');
const [typeFilter, setTypeFilter] = useState('');
const [sourceFilter, setSourceFilter] = useState('');
const [selectedIds, setSelectedIds] = useState<string[]>([]);
// Pagination state
@@ -129,8 +130,15 @@ export default function Tasks() {
try {
const ordering = sortBy ? `${sortDirection === 'desc' ? '-' : ''}${sortBy}` : '-created_at';
// Build search term - combine user search with Site Builder filter if needed
let finalSearchTerm = searchTerm;
if (sourceFilter === 'site_builder') {
// If user has a search term, combine it with Site Builder prefix
finalSearchTerm = searchTerm ? `[Site Builder] ${searchTerm}` : '[Site Builder]';
}
const filters: TasksFilters = {
...(searchTerm && { search: searchTerm }),
...(finalSearchTerm && { search: finalSearchTerm }),
...(statusFilter && { status: statusFilter }),
...(clusterFilter && { cluster_id: clusterFilter }),
...(structureFilter && { content_structure: structureFilter }),
@@ -495,6 +503,8 @@ export default function Tasks() {
statusFilter,
setStatusFilter,
clusterFilter,
sourceFilter,
setSourceFilter,
setClusterFilter,
structureFilter,
setStructureFilter,
@@ -502,7 +512,7 @@ export default function Tasks() {
setTypeFilter,
setCurrentPage,
});
}, [clusters, activeSector, formData, searchTerm, statusFilter, clusterFilter, structureFilter, typeFilter]);
}, [clusters, activeSector, formData, searchTerm, statusFilter, clusterFilter, structureFilter, typeFilter, sourceFilter]);
// Calculate header metrics
const headerMetrics = useMemo(() => {
@@ -565,6 +575,7 @@ export default function Tasks() {
cluster_id: clusterFilter,
content_structure: structureFilter,
content_type: typeFilter,
source: sourceFilter,
}}
onFilterChange={(key, value) => {
const stringValue = value === null || value === undefined ? '' : String(value);
@@ -578,6 +589,8 @@ export default function Tasks() {
setStructureFilter(stringValue);
} else if (key === 'content_type') {
setTypeFilter(stringValue);
} else if (key === 'source') {
setSourceFilter(stringValue);
}
setCurrentPage(1);
}}