/** * Site Settings (Advanced) * Phase 7: Advanced Site Management * Features: SEO (meta tags, Open Graph, schema.org), Industry & Sectors Configuration */ import React, { useState, useEffect, useRef } 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 Label from '../../components/form/Label'; import SelectDropdown from '../../components/form/SelectDropdown'; import Checkbox from '../../components/form/input/Checkbox'; import TextArea from '../../components/form/input/TextArea'; import { useToast } from '../../components/ui/toast/ToastContainer'; import { fetchAPI, fetchSites, fetchIndustries, Site, Industry, } from '../../services/api'; import WordPressIntegrationForm from '../../components/sites/WordPressIntegrationForm'; import { integrationApi, SiteIntegration } from '../../services/integration.api'; import { GridIcon, PlugInIcon, PaperPlaneIcon, DocsIcon, BoltIcon, FileIcon, ChevronDownIcon } from '../../icons'; import Badge from '../../components/ui/badge/Badge'; import { Dropdown } from '../../components/ui/dropdown/Dropdown'; import { DropdownItem } from '../../components/ui/dropdown/DropdownItem'; export default function SiteSettings() { const { id: siteId } = useParams<{ id: 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); // 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 const initialTab = (searchParams.get('tab') as 'general' | 'integrations' | 'content-types') || 'general'; const [activeTab, setActiveTab] = useState<'general' | 'integrations' | 'content-types'>(initialTab); const [contentTypes, setContentTypes] = useState(null); const [contentTypesLoading, setContentTypesLoading] = 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', 'integrations', 'content-types'].includes(tab)) { setActiveTab(tab as typeof activeTab); } }, [searchParams]); useEffect(() => { if (activeTab === 'content-types' && wordPressIntegration) { loadContentTypes(); } }, [activeTab, wordPressIntegration]); // 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); 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 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' }, ]; if (loading) { return (
Loading site settings...
); } return (
, color: 'blue' }} hideSiteSector /> {/* Integration status indicator */}
{integrationStatus === 'connected' && 'Connected'} {integrationStatus === 'configured' && (testingAuth ? 'Testing...' : 'Configured')} {integrationStatus === 'not_configured' && 'Not configured'}
{/* Site Selector - Only show if more than 1 site */} {!sitesLoading && sites.length > 1 && (
setIsSiteSelectorOpen(false)} anchorRef={siteSelectorRef} > {sites.map((s) => ( handleSiteSelect(s.id)} className={`flex items-center gap-3 px-3 py-2 font-medium rounded-lg text-sm text-left ${ site?.id === s.id ? "bg-brand-50 text-brand-700 dark:bg-brand-500/20 dark:text-brand-300" : "text-gray-700 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300" }`} > {s.name} {site?.id === s.id && ( )} ))}
)}
{/* Tabs */}
{(wordPressIntegration || site?.wp_url || site?.wp_api_key || site?.hosting_type === 'wordpress') && ( )}
{/* Content Types Tab */} {activeTab === 'content-types' && (

WordPress Content Types

View WordPress site structure and content counts

{wordPressIntegration && (
{wordPressIntegration.sync_status === 'success' ? 'Synced' : wordPressIntegration.sync_status === 'failed' ? 'Failed' : 'Pending'}
{(lastSyncTime || wordPressIntegration.last_sync_at) && (
{formatRelativeTime(lastSyncTime || wordPressIntegration.last_sync_at)}
)}
)}
{contentTypesLoading ? (

Loading content types...

) : ( <>
{!contentTypes ? (

No content types data available

Click "Sync Structure" to fetch WordPress content types

) : ( <> {/* Post Types Section */}

Post Types

{Object.entries(contentTypes.post_types || {}).map(([key, data]: [string, any]) => (

{data.label}

{data.count} total · {data.synced_count} synced
{data.last_synced && (

Last synced: {new Date(data.last_synced).toLocaleString()}

)}
{data.enabled ? 'Enabled' : 'Disabled'} Limit: {data.fetch_limit}
))}
{/* Taxonomies Section */}

Taxonomies

{Object.entries(contentTypes.taxonomies || {}).map(([key, data]: [string, any]) => (

{data.label}

{data.count} total · {data.synced_count} synced
{data.last_synced && (

Last synced: {new Date(data.last_synced).toLocaleString()}

)}
{data.enabled ? 'Enabled' : 'Disabled'} Limit: {data.fetch_limit}
))}
{/* Summary */} {contentTypes.last_structure_fetch && (

Structure last fetched: {new Date(contentTypes.last_structure_fetch).toLocaleString()}

)} )} )}
)} {/* Original tab content below */} {activeTab !== 'content-types' && (
{/* General Tab */} {activeTab === 'general' && ( <> {/* 4-Card Layout for Basic Settings, SEO, Open Graph, and Schema */}
{/* Card 1: Basic Site Settings */}

Basic Settings

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" />
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" />
setFormData({ ...formData, site_url: e.target.value })} placeholder="https://example.com" 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" />
setFormData({ ...formData, site_type: value })} />
setFormData({ ...formData, hosting_type: value })} />
setFormData({ ...formData, is_active: e.target.checked })} label="Active" />
{/* Card 2: SEO Meta Tags */}

SEO Meta Tags

setFormData({ ...formData, meta_title: e.target.value })} placeholder="SEO title (recommended: 50-60 characters)" maxLength={60} 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" />

{formData.meta_title.length}/60 characters