/** * Site Settings (Advanced) * Phase 7: Advanced Site Management * Features: SEO (meta tags, Open Graph, schema.org), Industry & Sectors Configuration */ import React, { useState, useEffect, useRef, useCallback } from 'react'; import { useParams, useNavigate, useSearchParams } from 'react-router-dom'; import PageMeta from '../../components/common/PageMeta'; import PageHeader from '../../components/common/PageHeader'; import { Card } from '../../components/ui/card'; import Button from '../../components/ui/button/Button'; import IconButton from '../../components/ui/button/IconButton'; import Label from '../../components/form/Label'; import InputField from '../../components/form/input/InputField'; import Select from '../../components/form/Select'; import SelectDropdown from '../../components/form/SelectDropdown'; import Checkbox from '../../components/form/input/Checkbox'; import Radio from '../../components/form/input/Radio'; import TextArea from '../../components/form/input/TextArea'; import Switch from '../../components/form/switch/Switch'; import { useToast } from '../../components/ui/toast/ToastContainer'; import { fetchAPI, fetchSites, fetchIndustries, Site, Industry, setActiveSite as apiSetActiveSite, } from '../../services/api'; import { useSiteStore } from '../../store/siteStore'; import WordPressIntegrationForm from '../../components/sites/WordPressIntegrationForm'; import { integrationApi, SiteIntegration } from '../../services/integration.api'; import { GridIcon, PlugInIcon, PaperPlaneIcon, DocsIcon, BoltIcon, FileIcon, ChevronDownIcon, CloseIcon, PlusIcon, RefreshCwIcon, FileTextIcon, ImageIcon, SaveIcon, Loader2Icon, ArrowRightIcon, SettingsIcon, GlobeIcon, LayersIcon, CheckCircleIcon, CalendarIcon, InfoIcon } from '../../icons'; import Badge from '../../components/ui/badge/Badge'; import { Dropdown } from '../../components/ui/dropdown/Dropdown'; import { DropdownItem } from '../../components/ui/dropdown/DropdownItem'; import SiteInfoBar from '../../components/common/SiteInfoBar'; export default function SiteSettings() { const { id: siteId } = useParams<{ id: string }>(); const navigate = useNavigate(); const [searchParams] = useSearchParams(); const toast = useToast(); const { setActiveSite } = useSiteStore(); 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); // Site selector state const [sites, setSites] = useState([]); const [sitesLoading, setSitesLoading] = useState(true); const [isSiteSelectorOpen, setIsSiteSelectorOpen] = useState(false); const siteSelectorRef = useRef(null); // Check for tab parameter in URL - image-settings merged into content-generation (renamed to ai-settings) const initialTab = (searchParams.get('tab') as 'general' | 'ai-settings' | 'integrations' | 'publishing' | 'content-types') || 'general'; const [activeTab, setActiveTab] = useState<'general' | 'ai-settings' | 'integrations' | 'publishing' | 'content-types'>(initialTab); const [contentTypes, setContentTypes] = useState(null); const [contentTypesLoading, setContentTypesLoading] = useState(false); // Advanced Settings toggle const [showAdvancedSettings, setShowAdvancedSettings] = useState(false); // Publishing settings state const [publishingSettings, setPublishingSettings] = useState(null); const [publishingSettingsLoading, setPublishingSettingsLoading] = useState(false); const [publishingSettingsSaving, setPublishingSettingsSaving] = useState(false); // AI Settings state (merged content generation + image settings per plan) // Content writing settings const [contentGenerationSettings, setContentGenerationSettings] = useState({ appendToPrompt: '', defaultTone: 'professional', defaultLength: 'medium', }); const [contentGenerationLoading, setContentGenerationLoading] = useState(false); const [contentGenerationSaving, setContentGenerationSaving] = useState(false); // AI Parameters (from SystemAISettings) const [temperature, setTemperature] = useState(0.7); const [maxTokens, setMaxTokens] = useState(8192); // Image Generation settings (from SystemAISettings + AIModelConfig) const [qualityTiers, setQualityTiers] = useState>([]); const [selectedTier, setSelectedTier] = useState('quality'); const [availableStyles, setAvailableStyles] = useState>([ { value: 'photorealistic', label: 'Photorealistic', description: 'Ultra realistic photography style' }, ]); const [selectedStyle, setSelectedStyle] = useState('photorealistic'); const [maxImages, setMaxImages] = useState(4); const [maxAllowed, setMaxAllowed] = useState(8); const [aiSettingsLoading, setAiSettingsLoading] = useState(false); const [aiSettingsSaving, setAiSettingsSaving] = useState(false); // Sectors selection state const [industries, setIndustries] = useState([]); const [selectedIndustry, setSelectedIndustry] = useState(''); const [selectedSectors, setSelectedSectors] = useState([]); const [isSelectingSectors, setIsSelectingSectors] = useState(false); const [userPreferences, setUserPreferences] = useState<{ selectedIndustry?: string; selectedSectors?: string[]; } | null>(null); const [formData, setFormData] = useState({ name: '', slug: '', site_url: '', site_type: 'marketing', hosting_type: 'igny8_sites', is_active: true, // SEO fields meta_title: '', meta_description: '', meta_keywords: '', og_title: '', og_description: '', og_image: '', og_type: 'website', og_site_name: '', schema_type: 'Organization', schema_name: '', schema_description: '', schema_url: '', schema_logo: '', schema_same_as: '', }); useEffect(() => { if (siteId) { // Clear state when site changes setWordPressIntegration(null); setContentTypes(null); setSite(null); // Load new site data loadSite(); loadIntegrations(); loadIndustries(); } }, [siteId]); useEffect(() => { // Update tab if URL parameter changes const tab = searchParams.get('tab'); if (tab && ['general', 'ai-settings', 'integrations', 'publishing', 'content-types'].includes(tab)) { setActiveTab(tab as typeof activeTab); } // Handle legacy tab names if (tab === 'content-generation' || tab === 'image-settings') { setActiveTab('ai-settings'); } }, [searchParams]); useEffect(() => { if (activeTab === 'content-types' && wordPressIntegration) { loadContentTypes(); } }, [activeTab, wordPressIntegration]); useEffect(() => { if (activeTab === 'publishing' && siteId && !publishingSettings) { loadPublishingSettings(); } }, [activeTab, siteId]); // Load AI settings when tab is active (merged content generation + image settings) useEffect(() => { if (activeTab === 'ai-settings' && siteId) { loadAISettings(); loadContentGenerationSettings(); } }, [activeTab, siteId]); // Load sites for selector useEffect(() => { loadSites(); loadUserPreferences(); }, []); // Load site sectors when site and industries are loaded useEffect(() => { if (site && industries.length > 0) { loadSiteSectors(); } }, [site, industries]); const loadSites = async () => { try { setSitesLoading(true); const response = await fetchSites(); const activeSites = (response.results || []).filter(site => site.is_active); setSites(activeSites); } catch (error: any) { console.error('Failed to load sites:', error); } finally { setSitesLoading(false); } }; const handleSiteSelect = (siteId: number) => { navigate(`/sites/${siteId}/settings${searchParams.get('tab') ? `?tab=${searchParams.get('tab')}` : ''}`); setIsSiteSelectorOpen(false); }; const loadSite = async () => { try { setLoading(true); const data = await fetchAPI(`/v1/auth/sites/${siteId}/`); if (data) { setSite(data); // Update global site store so site selector shows correct site setActiveSite(data); // Also set as active site in backend await apiSetActiveSite(data.id).catch(() => {}); const seoData = data.seo_metadata || data.metadata || {}; setFormData({ name: data.name || '', slug: data.slug || '', site_url: data.domain || data.url || '', site_type: data.site_type || 'marketing', hosting_type: data.hosting_type || 'igny8_sites', is_active: data.is_active !== false, // SEO fields meta_title: seoData.meta_title || data.name || '', meta_description: seoData.meta_description || data.description || '', meta_keywords: seoData.meta_keywords || '', og_title: seoData.og_title || seoData.meta_title || data.name || '', og_description: seoData.og_description || seoData.meta_description || data.description || '', og_image: seoData.og_image || '', og_type: seoData.og_type || 'website', og_site_name: seoData.og_site_name || data.name || '', schema_type: seoData.schema_type || 'Organization', schema_name: seoData.schema_name || data.name || '', schema_description: seoData.schema_description || data.description || '', schema_url: seoData.schema_url || data.domain || '', schema_logo: seoData.schema_logo || '', schema_same_as: Array.isArray(seoData.schema_same_as) ? seoData.schema_same_as.join(', ') : seoData.schema_same_as || '', }); // Don't automatically mark as connected - wait for actual connection test } } catch (error: any) { toast.error(`Failed to load site: ${error.message}`); } finally { setLoading(false); } }; 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 loadPublishingSettings = async () => { if (!siteId) return; try { setPublishingSettingsLoading(true); const response = await fetchAPI(`/v1/integration/sites/${siteId}/publishing-settings/`); setPublishingSettings(response.data || response); } catch (error: any) { console.error('Failed to load publishing settings:', error); // Set defaults if endpoint fails setPublishingSettings({ auto_approval_enabled: true, auto_publish_enabled: true, daily_publish_limit: 3, weekly_publish_limit: 15, monthly_publish_limit: 50, publish_days: ['mon', 'tue', 'wed', 'thu', 'fri'], publish_time_slots: ['09:00', '14:00', '18:00'], }); } finally { setPublishingSettingsLoading(false); } }; const savePublishingSettings = async (newSettings: any) => { if (!siteId) return; try { setPublishingSettingsSaving(true); const response = await fetchAPI(`/v1/integration/sites/${siteId}/publishing-settings/`, { method: 'PATCH', body: JSON.stringify(newSettings), }); setPublishingSettings(response.data || response); toast.success('Publishing settings saved successfully'); } catch (error: any) { console.error('Failed to save publishing settings:', error); toast.error('Failed to save publishing settings'); } finally { setPublishingSettingsSaving(false); } }; // Content Generation Settings const loadContentGenerationSettings = async () => { try { setContentGenerationLoading(true); const contentData = await fetchAPI('/v1/system/settings/content/content_generation/'); if (contentData?.config) { setContentGenerationSettings({ appendToPrompt: contentData.config.append_to_prompt || '', defaultTone: contentData.config.default_tone || 'professional', defaultLength: contentData.config.default_length || 'medium', }); } } catch (err) { console.log('Content generation settings not found, using defaults'); } finally { setContentGenerationLoading(false); } }; const saveContentGenerationSettings = async () => { try { setContentGenerationSaving(true); await fetchAPI('/v1/system/settings/content/content_generation/save/', { method: 'POST', body: JSON.stringify({ config: { append_to_prompt: contentGenerationSettings.appendToPrompt, default_tone: contentGenerationSettings.defaultTone, default_length: contentGenerationSettings.defaultLength, } }), }); toast.success('Content generation settings saved successfully'); } catch (error: any) { console.error('Error saving content generation settings:', error); toast.error(`Failed to save settings: ${error.message}`); } finally { setContentGenerationSaving(false); } }; // AI Settings (new merged API for temperature, max tokens, image quality/style) const loadAISettings = async () => { try { setAiSettingsLoading(true); const response = await fetchAPI('/v1/account/settings/ai/'); // Set content generation params (temperature, max_tokens) if (response?.content_generation) { setTemperature(response.content_generation.temperature ?? 0.7); setMaxTokens(response.content_generation.max_tokens ?? 8192); } // Set image generation params if (response?.image_generation) { setQualityTiers(response.image_generation.quality_tiers || []); setSelectedTier(response.image_generation.selected_tier || 'quality'); setAvailableStyles(response.image_generation.styles || []); setSelectedStyle(response.image_generation.selected_style || 'photorealistic'); setMaxImages(response.image_generation.max_images ?? 4); setMaxAllowed(response.image_generation.max_allowed ?? 4); } } catch (error: any) { console.error('Error loading AI settings:', error); } finally { setAiSettingsLoading(false); } }; const saveAISettings = async () => { try { setAiSettingsSaving(true); await fetchAPI('/v1/account/settings/ai/', { method: 'POST', body: JSON.stringify({ content_generation: { temperature, max_tokens: maxTokens, }, image_generation: { quality_tier: selectedTier, image_style: selectedStyle, max_images_per_article: maxImages, }, }), }); toast.success('AI settings saved successfully'); } catch (error: any) { console.error('Error saving AI settings:', error); toast.error(`Failed to save AI settings: ${error.message}`); } finally { setAiSettingsSaving(false); } }; // Legacy image settings functions (kept for backward compatibility but not used in AI Settings tab) const loadImageSettings = async () => { // No longer used - AI settings now loaded via loadAISettings() }; const saveImageSettings = async () => { // No longer used - AI settings now saved via saveAISettings() }; const loadIndustries = async () => { try { const response = await fetchIndustries(); let allIndustries = response.industries || []; // Note: For existing sites with industries already configured, // we show ALL industries so users can change their selection. // The filtering by user preferences only applies during initial site creation. setIndustries(allIndustries); } catch (error: any) { console.error('Failed to load industries:', error); } }; const loadUserPreferences = async () => { try { const { fetchAccountSetting } = await import('../../services/api'); const setting = await fetchAccountSetting('user_preferences'); const preferences = setting.config as { selectedIndustry?: string; selectedSectors?: string[] } | undefined; if (preferences) { setUserPreferences(preferences); } } catch (error: any) { // Silently handle errors } }; const loadSiteSectors = async () => { if (!siteId) return; try { const { fetchSiteSectors } = await import('../../services/api'); const sectors = await fetchSiteSectors(Number(siteId)); 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 getIndustrySectors = () => { if (!selectedIndustry) return []; const industry = industries.find(i => i.slug === selectedIndustry); // Note: For existing sites in Settings page, show ALL sectors from the selected industry // so users can change their sector selection. The filtering by user preferences // only applies during initial site creation (in the Sites wizard). return industry?.sectors || []; }; const handleSelectSectors = async () => { if (!siteId || !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); const { selectSectorsForSite } = await import('../../services/api'); await selectSectorsForSite( Number(siteId), selectedIndustry, selectedSectors ); toast.success('Sectors configured successfully'); await loadSite(); await loadSiteSectors(); } catch (error: any) { toast.error(`Failed to configure sectors: ${error.message}`); } finally { setIsSelectingSectors(false); } }; const handleIntegrationUpdate = async (integration: SiteIntegration) => { setWordPressIntegration(integration); await loadIntegrations(); }; const loadContentTypes = async () => { if (!wordPressIntegration?.id) return; try { setContentTypesLoading(true); const data = await fetchAPI(`/v1/integration/integrations/${wordPressIntegration.id}/content-types/`); setContentTypes(data); } catch (error: any) { toast.error(`Failed to load content types: ${error.message}`); } finally { setContentTypesLoading(false); } }; const formatRelativeTime = (iso: string | null) => { if (!iso) return '-'; const then = new Date(iso).getTime(); const now = Date.now(); const diff = Math.max(0, now - then); const mins = Math.floor(diff / 60000); if (mins < 1) return 'just now'; if (mins < 60) return `${mins}m ago`; const hours = Math.floor(mins / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); if (days < 30) return `${days}d ago`; const months = Math.floor(days / 30); return `${months}mo ago`; }; // Integration status with authentication check const [integrationStatus, setIntegrationStatus] = useState<'connected' | 'configured' | 'not_configured'>('not_configured'); const [testingAuth, setTestingAuth] = useState(false); // Check basic configuration - integration must exist in DB and have sync_enabled useEffect(() => { const checkStatus = async () => { // Integration must exist in database and have sync_enabled = true if (wordPressIntegration && wordPressIntegration.id && wordPressIntegration.sync_enabled) { setIntegrationStatus('configured'); // Test authentication testAuthentication(); } else { setIntegrationStatus('not_configured'); } }; checkStatus(); }, [wordPressIntegration, site]); // Auto-refresh integration list periodically to detect plugin-created integrations useEffect(() => { const interval = setInterval(() => { if (!wordPressIntegration) { loadIntegrations(); } }, 5000); // Check every 5 seconds if integration doesn't exist return () => clearInterval(interval); }, [wordPressIntegration]); // Test authentication with WordPress API const testAuthentication = async () => { if (testingAuth || !wordPressIntegration?.id) return; try { setTestingAuth(true); const resp = await fetchAPI(`/v1/integration/integrations/${wordPressIntegration.id}/test_connection/`, { method: 'POST', body: {} }); if (resp && resp.success) { setIntegrationStatus('connected'); } else { // Keep as 'configured' if auth fails setIntegrationStatus('configured'); } } catch (err) { // Keep as 'configured' if auth test fails setIntegrationStatus('configured'); } finally { setTestingAuth(false); } }; // Sync Now handler extracted const [syncLoading, setSyncLoading] = useState(false); const [lastSyncTime, setLastSyncTime] = useState(null); const handleManualSync = async () => { setSyncLoading(true); try { if (wordPressIntegration && wordPressIntegration.id) { const res = await integrationApi.syncIntegration(wordPressIntegration.id, 'metadata'); if (res && res.success) { toast.success('WordPress structure synced successfully'); if (res.last_sync_at) { setLastSyncTime(res.last_sync_at); } setTimeout(() => loadContentTypes(), 1500); } else { toast.error(res?.message || 'Sync failed to start'); } } else { toast.error('No integration configured. Please configure WordPress integration first.'); } } catch (err: any) { toast.error(`Sync failed: ${err?.message || String(err)}`); } finally { setSyncLoading(false); } }; const handleSave = async () => { try { setSaving(true); const { meta_title, meta_description, meta_keywords, og_title, og_description, og_image, og_type, og_site_name, schema_type, schema_name, schema_description, schema_url, schema_logo, schema_same_as, ...basicData } = formData; const payload = { ...basicData, seo_metadata: { meta_title, meta_description, meta_keywords, og_title, og_description, og_image, og_type, og_site_name, schema_type, schema_name, schema_description, schema_url, schema_logo, schema_same_as: schema_same_as ? schema_same_as.split(',').map((s) => s.trim()).filter(Boolean) : [], }, }; await fetchAPI(`/v1/auth/sites/${siteId}/`, { method: 'PUT', body: JSON.stringify(payload), }); 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' }, ]; return ( <> , color: 'blue' }} hideSiteSector /> {/* Site Info Bar */} {/* Tabs */}
{(wordPressIntegration || site?.wp_url || site?.wp_api_key || site?.hosting_type === 'wordpress') && ( )}
{/* Integration Status Indicator - Larger */}
{integrationStatus === 'connected' && 'Connected'} {integrationStatus === 'configured' && (testingAuth ? 'Testing...' : 'Configured')} {integrationStatus === 'not_configured' && 'Not Configured'}
{/* AI Settings Tab (merged content-generation + image-settings) */} {activeTab === 'ai-settings' && (
{/* 3 Cards in a Row */}
{/* Card 1: Content Settings */}

Content Settings

Customize article writing

{contentGenerationLoading ? (
) : (