diff --git a/backend/celerybeat-schedule b/backend/celerybeat-schedule index 2a1032aa..4f12d06b 100644 Binary files a/backend/celerybeat-schedule and b/backend/celerybeat-schedule differ diff --git a/frontend/src/components/sites/WordPressIntegrationCard.tsx b/frontend/src/components/sites/WordPressIntegrationCard.tsx new file mode 100644 index 00000000..04323f84 --- /dev/null +++ b/frontend/src/components/sites/WordPressIntegrationCard.tsx @@ -0,0 +1,144 @@ +/** + * WordPress Integration Card Component + * Displays WordPress integration status and quick actions + */ +import React from 'react'; +import { Globe, CheckCircle, XCircle, Settings, RefreshCw } from 'lucide-react'; +import { Card } from '../ui/card'; +import Button from '../ui/button/Button'; +import Badge from '../ui/badge/Badge'; + +interface WordPressIntegration { + id: number; + site: number; + platform: string; + is_active: boolean; + sync_enabled: boolean; + sync_status: 'success' | 'failed' | 'pending'; + last_sync_at?: string; + config_json?: { + url?: string; + username?: string; + }; +} + +interface WordPressIntegrationCardProps { + integration: WordPressIntegration | null; + onConnect: () => void; + onManage: () => void; + onSync?: () => void; + loading?: boolean; +} + +export default function WordPressIntegrationCard({ + integration, + onConnect, + onManage, + onSync, + loading = false, +}: WordPressIntegrationCardProps) { + if (!integration) { + return ( + + + + + + + + + WordPress Integration + + + Connect your WordPress site to sync content + + + + + Connect WordPress + + + + ); + } + + return ( + + + + + + + + + + WordPress Integration + + + {integration.config_json?.url || 'WordPress Site'} + + + + + + {integration.is_active ? 'Active' : 'Inactive'} + + + + + + + Sync Status + + {integration.sync_status === 'success' ? ( + + ) : integration.sync_status === 'failed' ? ( + + ) : ( + + )} + + {integration.sync_status} + + + + + Last Sync + + {integration.last_sync_at + ? new Date(integration.last_sync_at).toLocaleDateString() + : 'Never'} + + + + + + + + Manage + + {onSync && ( + + + + )} + + + + ); +} + diff --git a/frontend/src/components/sites/WordPressIntegrationModal.tsx b/frontend/src/components/sites/WordPressIntegrationModal.tsx new file mode 100644 index 00000000..46343c7e --- /dev/null +++ b/frontend/src/components/sites/WordPressIntegrationModal.tsx @@ -0,0 +1,177 @@ +/** + * WordPress Integration Modal Component + * Form for connecting/managing WordPress integration + */ +import React, { useState } from 'react'; +import { X, Globe, AlertCircle } from 'lucide-react'; +import Modal from '../ui/modal/Modal'; +import Button from '../ui/button/Button'; +import Label from '../form/Label'; +import Input from '../form/input/Input'; +import Checkbox from '../form/input/Checkbox'; +import { useToast } from '../ui/toast/ToastContainer'; + +interface WordPressIntegrationModalProps { + isOpen: boolean; + onClose: () => void; + onSubmit: (data: WordPressIntegrationFormData) => Promise; + initialData?: WordPressIntegrationFormData; + siteId: number; +} + +export interface WordPressIntegrationFormData { + url: string; + username: string; + app_password: string; + is_active: boolean; + sync_enabled: boolean; +} + +export default function WordPressIntegrationModal({ + isOpen, + onClose, + onSubmit, + initialData, + siteId, +}: WordPressIntegrationModalProps) { + const toast = useToast(); + const [loading, setLoading] = useState(false); + const [formData, setFormData] = useState({ + url: initialData?.url || '', + username: initialData?.username || '', + app_password: initialData?.app_password || '', + is_active: initialData?.is_active ?? true, + sync_enabled: initialData?.sync_enabled ?? true, + }); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!formData.url || !formData.username || !formData.app_password) { + toast.error('Please fill in all required fields'); + return; + } + + try { + setLoading(true); + await onSubmit(formData); + toast.success('WordPress integration saved successfully'); + onClose(); + } catch (error: any) { + toast.error(`Failed to save integration: ${error.message}`); + } finally { + setLoading(false); + } + }; + + return ( + + + + + + + + + {initialData ? 'Edit WordPress Integration' : 'Connect WordPress Site'} + + + + + + + + + + + + + WordPress Application Password Required + + You need to create an Application Password in WordPress. Go to Users → Profile → + Application Passwords to generate one. + + + + + + + + WordPress Site URL + + setFormData({ ...formData, url: e.target.value })} + placeholder="https://example.com" + required + /> + + Enter your WordPress site URL (without trailing slash) + + + + + + WordPress Username + + setFormData({ ...formData, username: e.target.value })} + placeholder="admin" + required + /> + + + + + Application Password + + setFormData({ ...formData, app_password: e.target.value })} + placeholder="xxxx xxxx xxxx xxxx xxxx xxxx" + required + /> + + Enter the application password (with spaces or without) + + + + + setFormData({ ...formData, is_active: e.target.checked })} + label="Enable Integration" + /> + setFormData({ ...formData, sync_enabled: e.target.checked })} + label="Enable Two-Way Sync" + /> + + + + + Cancel + + + {loading ? 'Saving...' : initialData ? 'Update Integration' : 'Connect'} + + + + + + ); +} + diff --git a/frontend/src/pages/Sites/Settings.tsx b/frontend/src/pages/Sites/Settings.tsx index e09f73f2..fd0240af 100644 --- a/frontend/src/pages/Sites/Settings.tsx +++ b/frontend/src/pages/Sites/Settings.tsx @@ -4,8 +4,8 @@ * Features: SEO (meta tags, Open Graph, schema.org) */ import React, { useState, useEffect } from 'react'; -import { useParams, useNavigate } from 'react-router-dom'; -import { SettingsIcon, SearchIcon, Share2Icon, CodeIcon } from 'lucide-react'; +import { useParams, useNavigate, useSearchParams } from 'react-router-dom'; +import { SettingsIcon, SearchIcon, Share2Icon, CodeIcon, PlugIcon } from 'lucide-react'; import PageMeta from '../../components/common/PageMeta'; import { Card } from '../../components/ui/card'; import Button from '../../components/ui/button/Button'; @@ -15,16 +15,25 @@ import Checkbox from '../../components/form/input/Checkbox'; import TextArea from '../../components/form/input/TextArea'; import { useToast } from '../../components/ui/toast/ToastContainer'; import { fetchAPI } from '../../services/api'; +import WordPressIntegrationCard from '../../components/sites/WordPressIntegrationCard'; +import WordPressIntegrationModal, { WordPressIntegrationFormData } from '../../components/sites/WordPressIntegrationModal'; +import { integrationApi, SiteIntegration } from '../../services/integration.api'; export default function SiteSettings() { const { siteId } = useParams<{ siteId: string }>(); const navigate = useNavigate(); + const [searchParams] = useSearchParams(); const toast = useToast(); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [site, setSite] = useState(null); + const [wordPressIntegration, setWordPressIntegration] = useState(null); + const [integrationLoading, setIntegrationLoading] = useState(false); + const [isIntegrationModalOpen, setIsIntegrationModalOpen] = useState(false); - const [activeTab, setActiveTab] = useState<'general' | 'seo' | 'og' | 'schema'>('general'); + // Check for tab parameter in URL + const initialTab = (searchParams.get('tab') as 'general' | 'seo' | 'og' | 'schema' | 'integrations') || 'general'; + const [activeTab, setActiveTab] = useState<'general' | 'seo' | 'og' | 'schema' | 'integrations'>(initialTab); const [formData, setFormData] = useState({ name: '', slug: '', @@ -51,9 +60,18 @@ export default function SiteSettings() { useEffect(() => { if (siteId) { loadSite(); + loadIntegrations(); } }, [siteId]); + useEffect(() => { + // Update tab if URL parameter changes + const tab = searchParams.get('tab'); + if (tab && ['general', 'seo', 'og', 'schema', 'integrations'].includes(tab)) { + setActiveTab(tab as typeof activeTab); + } + }, [searchParams]); + const loadSite = async () => { try { setLoading(true); @@ -91,6 +109,40 @@ export default function SiteSettings() { } }; + const loadIntegrations = async () => { + if (!siteId) return; + try { + setIntegrationLoading(true); + const integration = await integrationApi.getWordPressIntegration(Number(siteId)); + setWordPressIntegration(integration); + } catch (error: any) { + // Integration might not exist, that's okay + setWordPressIntegration(null); + } finally { + setIntegrationLoading(false); + } + }; + + const handleSaveIntegration = async (data: WordPressIntegrationFormData) => { + if (!siteId) return; + await integrationApi.saveWordPressIntegration(Number(siteId), data); + await loadIntegrations(); + }; + + const handleSyncIntegration = async () => { + if (!wordPressIntegration || !siteId) return; + try { + setIntegrationLoading(true); + await integrationApi.syncIntegration(wordPressIntegration.id); + toast.success('Content synced successfully'); + await loadIntegrations(); + } catch (error: any) { + toast.error(`Failed to sync: ${error.message}`); + } finally { + setIntegrationLoading(false); + } + }; + const handleSave = async () => { try { setSaving(true); @@ -219,6 +271,18 @@ export default function SiteSettings() { Schema.org + setActiveTab('integrations')} + className={`px-4 py-2 font-medium border-b-2 transition-colors ${ + activeTab === 'integrations' + ? 'border-brand-500 text-brand-600 dark:text-brand-400' + : 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300' + }`} + > + + Integrations + @@ -475,12 +539,49 @@ export default function SiteSettings() { )} - - - {saving ? 'Saving...' : 'Save Settings'} - - + {/* Integrations Tab */} + {activeTab === 'integrations' && ( + + setIsIntegrationModalOpen(true)} + onManage={() => setIsIntegrationModalOpen(true)} + onSync={handleSyncIntegration} + loading={integrationLoading} + /> + + )} + + {/* Save Button */} + {activeTab !== 'integrations' && ( + + + {saving ? 'Saving...' : 'Save Settings'} + + + )} + + {/* WordPress Integration Modal */} + {siteId && ( + setIsIntegrationModalOpen(false)} + onSubmit={handleSaveIntegration} + siteId={Number(siteId)} + initialData={ + wordPressIntegration + ? { + url: wordPressIntegration.config_json?.url || '', + username: wordPressIntegration.config_json?.username || '', + app_password: '', // Never show password + is_active: wordPressIntegration.is_active, + sync_enabled: wordPressIntegration.sync_enabled, + } + : undefined + } + /> + )} ); } diff --git a/frontend/src/services/integration.api.ts b/frontend/src/services/integration.api.ts new file mode 100644 index 00000000..4d64382b --- /dev/null +++ b/frontend/src/services/integration.api.ts @@ -0,0 +1,146 @@ +/** + * Integration API Service + * Handles all integration-related API calls + */ +import { fetchAPI } from './api'; + +export interface SiteIntegration { + id: number; + site: number; + platform: 'wordpress' | 'shopify' | 'custom'; + platform_type: 'cms' | 'ecommerce' | 'custom_api'; + config_json: Record; + is_active: boolean; + sync_enabled: boolean; + last_sync_at?: string; + sync_status: 'success' | 'failed' | 'pending'; + created_at: string; + updated_at: string; +} + +export interface CreateIntegrationData { + site: number; + platform: 'wordpress' | 'shopify' | 'custom'; + platform_type?: 'cms' | 'ecommerce' | 'custom_api'; + config_json: Record; + credentials?: Record; + is_active?: boolean; + sync_enabled?: boolean; +} + +export const integrationApi = { + /** + * Get all integrations for a site + */ + async getSiteIntegrations(siteId: number): Promise { + const response = await fetchAPI(`/v1/integration/integrations/?site=${siteId}`); + return response?.results || response || []; + }, + + /** + * Get integration by ID + */ + async getIntegration(integrationId: number): Promise { + return await fetchAPI(`/v1/integration/integrations/${integrationId}/`); + }, + + /** + * Create new integration + */ + async createIntegration(data: CreateIntegrationData): Promise { + return await fetchAPI('/v1/integration/integrations/', { + method: 'POST', + body: JSON.stringify(data), + }); + }, + + /** + * Update integration + */ + async updateIntegration( + integrationId: number, + data: Partial + ): Promise { + return await fetchAPI(`/v1/integration/integrations/${integrationId}/`, { + method: 'PATCH', + body: JSON.stringify(data), + }); + }, + + /** + * Delete integration + */ + async deleteIntegration(integrationId: number): Promise { + await fetchAPI(`/v1/integration/integrations/${integrationId}/`, { + method: 'DELETE', + }); + }, + + /** + * Test integration connection + */ + async testIntegration(integrationId: number): Promise<{ success: boolean; message: string }> { + return await fetchAPI(`/v1/integration/integrations/${integrationId}/test/`, { + method: 'POST', + }); + }, + + /** + * Sync content from integration + */ + async syncIntegration( + integrationId: number, + syncType: 'full' | 'incremental' = 'incremental' + ): Promise<{ success: boolean; message: string; synced_count?: number }> { + return await fetchAPI(`/v1/integration/integrations/${integrationId}/sync/`, { + method: 'POST', + body: JSON.stringify({ sync_type: syncType }), + }); + }, + + /** + * Get WordPress integration for a site + */ + async getWordPressIntegration(siteId: number): Promise { + const integrations = await this.getSiteIntegrations(siteId); + return integrations.find((i) => i.platform === 'wordpress') || null; + }, + + /** + * Create or update WordPress integration + */ + async saveWordPressIntegration( + siteId: number, + data: { + url: string; + username: string; + app_password: string; + is_active?: boolean; + sync_enabled?: boolean; + } + ): Promise { + const existing = await this.getWordPressIntegration(siteId); + + const integrationData: CreateIntegrationData = { + site: siteId, + platform: 'wordpress', + platform_type: 'cms', + config_json: { + url: data.url, + username: data.username, + }, + credentials: { + app_password: data.app_password, + }, + is_active: data.is_active ?? true, + sync_enabled: data.sync_enabled ?? true, + }; + + if (existing) { + return await this.updateIntegration(existing.id, integrationData); + } else { + return await this.createIntegration(integrationData); + } + }, +}; +
+ Connect your WordPress site to sync content +
+ {integration.config_json?.url || 'WordPress Site'} +
Sync Status
Last Sync
+ {integration.last_sync_at + ? new Date(integration.last_sync_at).toLocaleDateString() + : 'Never'} +
WordPress Application Password Required
+ You need to create an Application Password in WordPress. Go to Users → Profile → + Application Passwords to generate one. +
+ Enter your WordPress site URL (without trailing slash) +
+ Enter the application password (with spaces or without) +