1999 lines
84 KiB
TypeScript
1999 lines
84 KiB
TypeScript
/**
|
|
* 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<any>(null);
|
|
const [wordPressIntegration, setWordPressIntegration] = useState<SiteIntegration | null>(null);
|
|
const [integrationLoading, setIntegrationLoading] = useState(false);
|
|
|
|
// Site selector state
|
|
const [sites, setSites] = useState<Site[]>([]);
|
|
const [sitesLoading, setSitesLoading] = useState(true);
|
|
const [isSiteSelectorOpen, setIsSiteSelectorOpen] = useState(false);
|
|
const siteSelectorRef = useRef<HTMLButtonElement>(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<any>(null);
|
|
const [contentTypesLoading, setContentTypesLoading] = useState(false);
|
|
|
|
// Advanced Settings toggle
|
|
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
|
|
|
|
// Publishing settings state
|
|
const [publishingSettings, setPublishingSettings] = useState<any>(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<Array<{value: string; credits: number; label: string; description: string}>>([]);
|
|
const [selectedTier, setSelectedTier] = useState('quality');
|
|
const [availableStyles, setAvailableStyles] = useState<Array<{value: string; label: string; description?: string}>>([
|
|
{ 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<Industry[]>([]);
|
|
const [selectedIndustry, setSelectedIndustry] = useState<string>('');
|
|
const [selectedSectors, setSelectedSectors] = useState<string[]>([]);
|
|
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<string | null>(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 (
|
|
<>
|
|
<PageMeta title="Site Settings - IGNY8" />
|
|
<PageHeader
|
|
title="Site Settings"
|
|
badge={{ icon: <GridIcon />, color: 'blue' }}
|
|
hideSiteSector
|
|
/>
|
|
|
|
{/* Site Info Bar */}
|
|
<SiteInfoBar site={site} currentPage="settings" />
|
|
|
|
{/* Tabs */}
|
|
<div className="mb-6 border-b border-gray-200 dark:border-gray-700">
|
|
<div className="flex items-center justify-between gap-4">
|
|
<div className="flex gap-4 overflow-x-auto">
|
|
<Button
|
|
variant="ghost"
|
|
onClick={() => {
|
|
setActiveTab('general');
|
|
navigate(`/sites/${siteId}/settings`, { replace: true });
|
|
}}
|
|
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors whitespace-nowrap ${
|
|
activeTab === 'general'
|
|
? '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'
|
|
}`}
|
|
startIcon={<GridIcon className={`w-4 h-4 ${activeTab === 'general' ? 'text-brand-500' : ''}`} />}
|
|
>
|
|
General
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
onClick={() => {
|
|
setActiveTab('ai-settings');
|
|
navigate(`/sites/${siteId}/settings?tab=ai-settings`, { replace: true });
|
|
}}
|
|
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors whitespace-nowrap ${
|
|
activeTab === 'ai-settings'
|
|
? 'border-success-500 text-success-600 dark:text-success-400'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
|
}`}
|
|
startIcon={<BoltIcon className={`w-4 h-4 ${activeTab === 'ai-settings' ? 'text-success-500' : ''}`} />}
|
|
>
|
|
AI Settings
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
onClick={() => {
|
|
setActiveTab('integrations');
|
|
navigate(`/sites/${siteId}/settings?tab=integrations`, { replace: true });
|
|
}}
|
|
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors whitespace-nowrap ${
|
|
activeTab === 'integrations'
|
|
? 'border-warning-500 text-warning-600 dark:text-warning-400'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
|
}`}
|
|
startIcon={<PlugInIcon className={`w-4 h-4 ${activeTab === 'integrations' ? 'text-warning-500' : ''}`} />}
|
|
>
|
|
Integrations
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
onClick={() => {
|
|
setActiveTab('publishing');
|
|
navigate(`/sites/${siteId}/settings?tab=publishing`, { replace: true });
|
|
}}
|
|
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors whitespace-nowrap ${
|
|
activeTab === 'publishing'
|
|
? 'border-info-500 text-info-600 dark:text-info-400'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
|
}`}
|
|
startIcon={<PaperPlaneIcon className={`w-4 h-4 ${activeTab === 'publishing' ? 'text-info-500' : ''}`} />}
|
|
>
|
|
Publishing
|
|
</Button>
|
|
{(wordPressIntegration || site?.wp_url || site?.wp_api_key || site?.hosting_type === 'wordpress') && (
|
|
<Button
|
|
variant="ghost"
|
|
onClick={() => {
|
|
setActiveTab('content-types');
|
|
navigate(`/sites/${siteId}/settings?tab=content-types`, { replace: true });
|
|
}}
|
|
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors whitespace-nowrap ${
|
|
activeTab === 'content-types'
|
|
? 'border-error-500 text-error-600 dark:text-error-400'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
|
}`}
|
|
startIcon={<FileIcon className={`w-4 h-4 ${activeTab === 'content-types' ? 'text-error-500' : ''}`} />}
|
|
>
|
|
Content Types
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Integration Status Indicator - Larger */}
|
|
<div className="flex items-center gap-3 px-4 py-2 rounded-lg bg-gray-50 dark:bg-gray-800 flex-shrink-0">
|
|
<span
|
|
className={`inline-block w-4 h-4 rounded-full ${
|
|
integrationStatus === 'connected' ? 'bg-success-500' :
|
|
integrationStatus === 'configured' ? 'bg-brand-500' : 'bg-gray-300'
|
|
}`}
|
|
/>
|
|
<span className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
|
{integrationStatus === 'connected' && 'Connected'}
|
|
{integrationStatus === 'configured' && (testingAuth ? 'Testing...' : 'Configured')}
|
|
{integrationStatus === 'not_configured' && 'Not Configured'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* AI Settings Tab (merged content-generation + image-settings) */}
|
|
{activeTab === 'ai-settings' && (
|
|
<div className="space-y-6">
|
|
{/* 3 Cards in a Row */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
|
|
{/* Card 1: Content Settings */}
|
|
<Card className="p-6 border-l-4 border-l-brand-500">
|
|
<div className="flex items-center gap-3 mb-6">
|
|
<div className="p-2 bg-brand-100 dark:bg-brand-900/30 rounded-lg">
|
|
<FileTextIcon className="w-5 h-5 text-brand-600 dark:text-brand-400" />
|
|
</div>
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Content Settings</h2>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">Customize article writing</p>
|
|
</div>
|
|
</div>
|
|
|
|
{contentGenerationLoading ? (
|
|
<div className="flex items-center justify-center py-8">
|
|
<Loader2Icon className="w-8 h-8 animate-spin text-brand-500" />
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Label className="mb-2">Append to Prompt</Label>
|
|
<TextArea
|
|
value={contentGenerationSettings.appendToPrompt}
|
|
onChange={(value) => setContentGenerationSettings({ ...contentGenerationSettings, appendToPrompt: value })}
|
|
placeholder="Custom instructions..."
|
|
rows={3}
|
|
/>
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
Appended to every AI prompt
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<Label className="mb-2">Tone</Label>
|
|
<SelectDropdown
|
|
options={[
|
|
{ value: 'professional', label: 'Professional' },
|
|
{ value: 'conversational', label: 'Conversational' },
|
|
{ value: 'formal', label: 'Formal' },
|
|
{ value: 'casual', label: 'Casual' },
|
|
{ value: 'friendly', label: 'Friendly' },
|
|
]}
|
|
value={contentGenerationSettings.defaultTone}
|
|
onChange={(value) => setContentGenerationSettings({ ...contentGenerationSettings, defaultTone: value })}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label className="mb-2">Article Length</Label>
|
|
<SelectDropdown
|
|
options={[
|
|
{ value: 'short', label: 'Short (500-800)' },
|
|
{ value: 'medium', label: 'Medium (1000-1500)' },
|
|
{ value: 'long', label: 'Long (2000-3000)' },
|
|
{ value: 'comprehensive', label: 'Comprehensive (3000+)' },
|
|
]}
|
|
value={contentGenerationSettings.defaultLength}
|
|
onChange={(value) => setContentGenerationSettings({ ...contentGenerationSettings, defaultLength: value })}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
|
|
{/* Card 2: AI Parameters */}
|
|
<Card className="p-6 border-l-4 border-l-success-500">
|
|
<div className="flex items-center gap-3 mb-6">
|
|
<div className="p-2 bg-success-100 dark:bg-success-900/30 rounded-lg">
|
|
<BoltIcon className="w-5 h-5 text-success-600 dark:text-success-400" />
|
|
</div>
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">AI Parameters</h2>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">Fine-tune content generation behavior</p>
|
|
</div>
|
|
</div>
|
|
|
|
{aiSettingsLoading ? (
|
|
<div className="flex items-center justify-center py-8">
|
|
<Loader2Icon className="w-8 h-8 animate-spin text-success-500" />
|
|
</div>
|
|
) : (
|
|
<div className="space-y-6">
|
|
{/* Temperature Slider */}
|
|
<div>
|
|
<Label className="mb-2">Temperature</Label>
|
|
<div className="flex items-center gap-4">
|
|
<div className="flex-1">
|
|
<input
|
|
type="range"
|
|
min="0"
|
|
max="2"
|
|
step="0.1"
|
|
value={temperature}
|
|
onChange={(e) => setTemperature(parseFloat(e.target.value))}
|
|
className="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-success-500"
|
|
/>
|
|
<div className="flex justify-between text-xs text-gray-500 mt-1">
|
|
<span>More focused</span>
|
|
<span>More creative</span>
|
|
</div>
|
|
</div>
|
|
<span className="w-12 text-center font-medium text-gray-700 dark:text-gray-300">{temperature.toFixed(1)}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Max Tokens Dropdown */}
|
|
<div className="max-w-xs">
|
|
<Label className="mb-2">Max Tokens</Label>
|
|
<SelectDropdown
|
|
options={[
|
|
{ value: '2048', label: '2,048 tokens' },
|
|
{ value: '4096', label: '4,096 tokens' },
|
|
{ value: '8192', label: '8,192 tokens' },
|
|
{ value: '16384', label: '16,384 tokens' },
|
|
]}
|
|
value={String(maxTokens)}
|
|
onChange={(value) => setMaxTokens(parseInt(value))}
|
|
className="w-full"
|
|
/>
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
Maximum length of generated content. Higher values allow longer articles.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
|
|
{/* Card 3: Image Generation */}
|
|
<Card className="p-6 border-l-4 border-l-purple-500">
|
|
<div className="flex items-center gap-3 mb-6">
|
|
<div className="p-2 bg-purple-100 dark:bg-purple-900/30 rounded-lg">
|
|
<ImageIcon className="w-5 h-5 text-purple-600 dark:text-purple-400" />
|
|
</div>
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Image Generation</h2>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">Quality & style</p>
|
|
</div>
|
|
</div>
|
|
|
|
{aiSettingsLoading ? (
|
|
<div className="flex items-center justify-center py-8">
|
|
<Loader2Icon className="w-8 h-8 animate-spin text-purple-500" />
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{/* Quality Tier Dropdown */}
|
|
<div>
|
|
<Label className="mb-2">Quality</Label>
|
|
<SelectDropdown
|
|
options={qualityTiers.length > 0
|
|
? qualityTiers.map(tier => ({
|
|
value: tier.value,
|
|
label: `${tier.label} (${tier.credits} credits)`
|
|
}))
|
|
: [
|
|
{ value: 'basic', label: 'Basic (1 credit)' },
|
|
{ value: 'quality', label: 'Quality (5 credits)' },
|
|
{ value: 'premium', label: 'Premium (15 credits)' },
|
|
]
|
|
}
|
|
value={selectedTier}
|
|
onChange={(value) => setSelectedTier(value)}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
|
|
{/* Image Style Dropdown */}
|
|
<div>
|
|
<Label className="mb-2">Style</Label>
|
|
<SelectDropdown
|
|
options={availableStyles.length > 0
|
|
? availableStyles.map(style => ({ value: style.value, label: style.label }))
|
|
: [
|
|
{ value: 'photorealistic', label: 'Photorealistic' },
|
|
{ value: 'illustration', label: 'Illustration' },
|
|
{ value: '3d_render', label: '3D Render' },
|
|
{ value: 'minimal_flat', label: 'Minimal / Flat' },
|
|
{ value: 'artistic', label: 'Artistic' },
|
|
{ value: 'cartoon', label: 'Cartoon' },
|
|
]
|
|
}
|
|
value={selectedStyle}
|
|
onChange={(value) => setSelectedStyle(value)}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
|
|
{/* Images Per Article Dropdown */}
|
|
<div>
|
|
<Label className="mb-2">Images per Article</Label>
|
|
<SelectDropdown
|
|
options={Array.from({ length: 4 }, (_, i) => ({
|
|
value: String(i + 1),
|
|
label: `${i + 1} image${i > 0 ? 's' : ''}`,
|
|
}))}
|
|
value={String(maxImages)}
|
|
onChange={(value) => setMaxImages(parseInt(value))}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
|
|
</div>
|
|
{/* End of 3-card grid */}
|
|
|
|
{/* Save Button */}
|
|
<div className="flex justify-end gap-3">
|
|
<Button
|
|
variant="primary"
|
|
tone="brand"
|
|
onClick={async () => {
|
|
await Promise.all([
|
|
saveAISettings(),
|
|
saveContentGenerationSettings(),
|
|
]);
|
|
}}
|
|
disabled={aiSettingsSaving || contentGenerationSaving}
|
|
startIcon={(aiSettingsSaving || contentGenerationSaving) ? <Loader2Icon className="w-4 h-4 animate-spin" /> : <SaveIcon className="w-4 h-4" />}
|
|
>
|
|
{(aiSettingsSaving || contentGenerationSaving) ? 'Saving...' : 'Save Settings'}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Publishing Tab */}
|
|
{activeTab === 'publishing' && (
|
|
<div className="space-y-6">
|
|
{publishingSettingsLoading ? (
|
|
<Card>
|
|
<div className="p-6">
|
|
<div className="text-center py-8 text-gray-500">
|
|
<Loader2Icon className="w-8 h-8 animate-spin mx-auto mb-3 text-brand-600" />
|
|
<p>Loading publishing settings...</p>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
) : publishingSettings ? (
|
|
<>
|
|
{/* 3 Cards in a Row */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
|
|
{/* Card 1: Automation */}
|
|
<Card className="p-6 border-l-4 border-l-brand-500">
|
|
<div className="flex items-center gap-3 mb-4">
|
|
<div className="p-2 bg-brand-100 dark:bg-brand-900/30 rounded-lg">
|
|
<BoltIcon className="w-5 h-5 text-brand-600 dark:text-brand-400" />
|
|
</div>
|
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Automation</h2>
|
|
</div>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400 mb-6">
|
|
Configure automatic content approval and publishing to WordPress
|
|
</p>
|
|
|
|
<div className="space-y-6">
|
|
<div>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<Label>Auto-Approval</Label>
|
|
<Switch
|
|
label=""
|
|
checked={publishingSettings.auto_approval_enabled}
|
|
onChange={(checked) => {
|
|
const newSettings = { ...publishingSettings, auto_approval_enabled: checked };
|
|
setPublishingSettings(newSettings);
|
|
savePublishingSettings({ auto_approval_enabled: checked });
|
|
}}
|
|
/>
|
|
</div>
|
|
<p className="text-xs text-gray-500">
|
|
Automatically approve content after review
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<Label>Auto-Publish</Label>
|
|
<Switch
|
|
label=""
|
|
checked={publishingSettings.auto_publish_enabled}
|
|
onChange={(checked) => {
|
|
const newSettings = { ...publishingSettings, auto_publish_enabled: checked };
|
|
setPublishingSettings(newSettings);
|
|
savePublishingSettings({ auto_publish_enabled: checked });
|
|
}}
|
|
/>
|
|
</div>
|
|
<p className="text-xs text-gray-500">
|
|
Publish approved content to WordPress
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Card 2: Publishing Limits */}
|
|
<Card className="p-6 border-l-4 border-l-success-500">
|
|
<div className="flex items-center gap-3 mb-4">
|
|
<div className="p-2 bg-success-100 dark:bg-success-900/30 rounded-lg">
|
|
<LayersIcon className="w-5 h-5 text-success-600 dark:text-success-400" />
|
|
</div>
|
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Limits</h2>
|
|
</div>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400 mb-6">
|
|
Set maximum articles to publish per day, week, and month
|
|
</p>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Label>Daily</Label>
|
|
<InputField
|
|
type="number"
|
|
min="1"
|
|
max="50"
|
|
value={publishingSettings.daily_publish_limit}
|
|
onChange={(e) => {
|
|
const value = Math.max(1, Math.min(50, parseInt(e.target.value) || 1));
|
|
setPublishingSettings({ ...publishingSettings, daily_publish_limit: value });
|
|
}}
|
|
/>
|
|
<p className="text-xs text-gray-500 mt-1">Articles per day</p>
|
|
</div>
|
|
<div>
|
|
<Label>Weekly</Label>
|
|
<InputField
|
|
type="number"
|
|
min="1"
|
|
max="200"
|
|
value={publishingSettings.weekly_publish_limit}
|
|
onChange={(e) => {
|
|
const value = Math.max(1, Math.min(200, parseInt(e.target.value) || 1));
|
|
setPublishingSettings({ ...publishingSettings, weekly_publish_limit: value });
|
|
}}
|
|
/>
|
|
<p className="text-xs text-gray-500 mt-1">Articles per week</p>
|
|
</div>
|
|
<div>
|
|
<Label>Monthly</Label>
|
|
<InputField
|
|
type="number"
|
|
min="1"
|
|
max="500"
|
|
value={publishingSettings.monthly_publish_limit}
|
|
onChange={(e) => {
|
|
const value = Math.max(1, Math.min(500, parseInt(e.target.value) || 1));
|
|
setPublishingSettings({ ...publishingSettings, monthly_publish_limit: value });
|
|
}}
|
|
/>
|
|
<p className="text-xs text-gray-500 mt-1">Articles per month</p>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Card 3: Schedule (Days + Time Slots) */}
|
|
<Card className="p-6 border-l-4 border-l-purple-500">
|
|
<div className="flex items-center gap-3 mb-4">
|
|
<div className="p-2 bg-purple-100 dark:bg-purple-900/30 rounded-lg">
|
|
<CalendarIcon className="w-5 h-5 text-purple-600 dark:text-purple-400" />
|
|
</div>
|
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Schedule</h2>
|
|
</div>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400 mb-6">
|
|
Select which days and times to automatically publish content
|
|
</p>
|
|
|
|
<div className="space-y-6">
|
|
<div>
|
|
<Label className="mb-2">Publishing Days</Label>
|
|
<div className="flex flex-wrap gap-2">
|
|
{[
|
|
{ value: 'mon', label: 'M' },
|
|
{ value: 'tue', label: 'T' },
|
|
{ value: 'wed', label: 'W' },
|
|
{ value: 'thu', label: 'T' },
|
|
{ value: 'fri', label: 'F' },
|
|
{ value: 'sat', label: 'S' },
|
|
{ value: 'sun', label: 'S' },
|
|
].map((day) => (
|
|
<Button
|
|
key={day.value}
|
|
variant={(publishingSettings.publish_days || []).includes(day.value) ? 'primary' : 'outline'}
|
|
tone="brand"
|
|
size="sm"
|
|
onClick={() => {
|
|
const currentDays = publishingSettings.publish_days || [];
|
|
const newDays = currentDays.includes(day.value)
|
|
? currentDays.filter((d: string) => d !== day.value)
|
|
: [...currentDays, day.value];
|
|
setPublishingSettings({ ...publishingSettings, publish_days: newDays });
|
|
savePublishingSettings({ publish_days: newDays });
|
|
}}
|
|
className="w-10 h-10 p-0"
|
|
>
|
|
{day.label}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<Label className="mb-2">Time Slots</Label>
|
|
<p className="text-xs text-gray-500 mb-3">In your local timezone</p>
|
|
<div className="space-y-2">
|
|
{(publishingSettings.publish_time_slots || ['09:00', '14:00', '18:00']).map((time: string, index: number) => (
|
|
<div key={index} className="flex items-center gap-2">
|
|
<InputField
|
|
type="time"
|
|
value={time}
|
|
onChange={(e) => {
|
|
const newSlots = [...(publishingSettings.publish_time_slots || [])];
|
|
newSlots[index] = e.target.value;
|
|
setPublishingSettings({ ...publishingSettings, publish_time_slots: newSlots });
|
|
}}
|
|
/>
|
|
{(publishingSettings.publish_time_slots || []).length > 1 && (
|
|
<IconButton
|
|
icon={<CloseIcon className="w-4 h-4" />}
|
|
variant="ghost"
|
|
tone="danger"
|
|
size="sm"
|
|
title="Remove time slot"
|
|
onClick={() => {
|
|
const newSlots = (publishingSettings.publish_time_slots || []).filter((_: string, i: number) => i !== index);
|
|
setPublishingSettings({ ...publishingSettings, publish_time_slots: newSlots });
|
|
savePublishingSettings({ publish_time_slots: newSlots });
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
))}
|
|
<Button
|
|
variant="ghost"
|
|
tone="brand"
|
|
size="sm"
|
|
startIcon={<PlusIcon className="w-4 h-4" />}
|
|
onClick={() => {
|
|
const newSlots = [...(publishingSettings.publish_time_slots || []), '12:00'];
|
|
setPublishingSettings({ ...publishingSettings, publish_time_slots: newSlots });
|
|
savePublishingSettings({ publish_time_slots: newSlots });
|
|
}}
|
|
>
|
|
Add Time Slot
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Info Box */}
|
|
<Card className="p-4 bg-brand-50 dark:bg-brand-900/20 border-brand-200 dark:border-brand-800">
|
|
<div className="flex items-start gap-3">
|
|
<InfoIcon className="w-5 h-5 text-brand-600 dark:text-brand-400 mt-0.5 flex-shrink-0" />
|
|
<div className="text-sm text-brand-800 dark:text-brand-200">
|
|
<p className="font-medium mb-1">How Publishing Works</p>
|
|
<ul className="list-disc list-inside space-y-1 text-brand-700 dark:text-brand-300">
|
|
<li>Content moves from Draft → Review → Approved → Published</li>
|
|
<li>Auto-approval moves content from Review to Approved automatically</li>
|
|
<li>Auto-publish sends Approved content to your WordPress site</li>
|
|
<li>You can always manually publish content using the "Publish to Site" button</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</>
|
|
) : (
|
|
<Card>
|
|
<div className="p-6">
|
|
<div className="text-center py-8 text-gray-500">
|
|
<p>Failed to load publishing settings. Please try again.</p>
|
|
<Button
|
|
variant="outline"
|
|
className="mt-4"
|
|
onClick={loadPublishingSettings}
|
|
>
|
|
Retry
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Content Types Tab */}
|
|
{activeTab === 'content-types' && (
|
|
<Card>
|
|
<div className="p-6">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h2 className="text-lg font-semibold">WordPress Content Types</h2>
|
|
<p className="text-sm text-gray-500 mt-1">View WordPress site structure and content counts</p>
|
|
</div>
|
|
{wordPressIntegration && (
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex items-center gap-2 px-3 py-1.5 rounded-md bg-gray-100 dark:bg-gray-800">
|
|
<div className={`w-2 h-2 rounded-full ${
|
|
wordPressIntegration.sync_status === 'success' ? 'bg-success-500' :
|
|
wordPressIntegration.sync_status === 'failed' ? 'bg-error-500' :
|
|
'bg-warning-500'
|
|
}`}></div>
|
|
<span className="text-xs font-medium text-gray-700 dark:text-gray-300">
|
|
{wordPressIntegration.sync_status === 'success' ? 'Synced' :
|
|
wordPressIntegration.sync_status === 'failed' ? 'Failed' : 'Pending'}
|
|
</span>
|
|
</div>
|
|
{(lastSyncTime || wordPressIntegration.last_sync_at) && (
|
|
<div className="text-xs text-gray-500">
|
|
{formatRelativeTime(lastSyncTime || wordPressIntegration.last_sync_at)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
{contentTypesLoading ? (
|
|
<div className="text-center py-8 text-gray-500">
|
|
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-brand-600 mb-3"></div>
|
|
<p>Loading content types...</p>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="flex items-center justify-end gap-3 mb-6">
|
|
<Button
|
|
variant="outline"
|
|
disabled={syncLoading || !(wordPressIntegration || site?.wp_url || site?.wp_api_key || site?.hosting_type === 'wordpress')}
|
|
onClick={handleManualSync}
|
|
startIcon={syncLoading ? <RefreshCwIcon className="w-4 h-4 animate-spin" /> : <RefreshCwIcon className="w-4 h-4" />}
|
|
>
|
|
{syncLoading ? 'Syncing...' : 'Sync Structure'}
|
|
</Button>
|
|
</div>
|
|
|
|
{!contentTypes ? (
|
|
<div className="text-center py-12 text-gray-500">
|
|
<svg className="w-16 h-16 mx-auto mb-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
|
|
</svg>
|
|
<p className="font-medium mb-1">No content types data available</p>
|
|
<p className="text-sm">Click "Sync Structure" to fetch WordPress content types</p>
|
|
</div>
|
|
) : (
|
|
<>
|
|
{/* Post Types Section */}
|
|
<div>
|
|
<h3 className="text-md font-medium mb-3">Post Types</h3>
|
|
<div className="space-y-3">
|
|
{Object.entries(contentTypes.post_types || {}).map(([key, data]: [string, any]) => (
|
|
<div key={key} className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-3">
|
|
<h4 className="font-medium">{data.label}</h4>
|
|
<span className="text-sm text-gray-500">
|
|
{data.count} total · {data.synced_count} synced
|
|
</span>
|
|
</div>
|
|
{data.last_synced && (
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
Last synced: {new Date(data.last_synced).toLocaleString()}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<span className={`px-2 py-1 text-xs rounded ${data.enabled ? 'bg-success-100 text-success-800 dark:bg-success-900 dark:text-success-200' : 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200'}`}>
|
|
{data.enabled ? 'Enabled' : 'Disabled'}
|
|
</span>
|
|
<span className="text-sm text-gray-500">Limit: {data.fetch_limit}</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Taxonomies Section */}
|
|
<div>
|
|
<h3 className="text-md font-medium mb-3">Taxonomies</h3>
|
|
<div className="space-y-3">
|
|
{Object.entries(contentTypes.taxonomies || {}).map(([key, data]: [string, any]) => (
|
|
<div key={key} className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-3">
|
|
<h4 className="font-medium">{data.label}</h4>
|
|
<span className="text-sm text-gray-500">
|
|
{data.count} total · {data.synced_count} synced
|
|
</span>
|
|
</div>
|
|
{data.last_synced && (
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
Last synced: {new Date(data.last_synced).toLocaleString()}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<span className={`px-2 py-1 text-xs rounded ${data.enabled ? 'bg-success-100 text-success-800 dark:bg-success-900 dark:text-success-200' : 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200'}`}>
|
|
{data.enabled ? 'Enabled' : 'Disabled'}
|
|
</span>
|
|
<span className="text-sm text-gray-500">Limit: {data.fetch_limit}</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Summary */}
|
|
{contentTypes.last_structure_fetch && (
|
|
<div className="mt-4 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
|
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
Structure last fetched: {new Date(contentTypes.last_structure_fetch).toLocaleString()}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Original tab content below */}
|
|
{activeTab !== 'content-types' && (
|
|
<div className="space-y-6">
|
|
{/* General Tab */}
|
|
{activeTab === 'general' && (
|
|
<>
|
|
{/* Row 1: Basic Settings and Industry/Sectors side by side */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
|
{/* Card 1: Basic Site Settings */}
|
|
<Card className="p-6 border-l-4 border-l-brand-500">
|
|
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
|
<GridIcon className="w-5 h-5 text-brand-500" />
|
|
Basic Settings
|
|
</h3>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Label>Site Name</Label>
|
|
<InputField
|
|
type="text"
|
|
value={formData.name}
|
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>Slug</Label>
|
|
<InputField
|
|
type="text"
|
|
value={formData.slug}
|
|
onChange={(e) => setFormData({ ...formData, slug: e.target.value })}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>Site URL</Label>
|
|
<InputField
|
|
type="text"
|
|
value={formData.site_url}
|
|
onChange={(e) => setFormData({ ...formData, site_url: e.target.value })}
|
|
placeholder="https://example.com"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>Site Type</Label>
|
|
<SelectDropdown
|
|
options={SITE_TYPES}
|
|
value={formData.site_type}
|
|
onChange={(value) => setFormData({ ...formData, site_type: value })}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>Hosting Type</Label>
|
|
<SelectDropdown
|
|
options={HOSTING_TYPES}
|
|
value={formData.hosting_type}
|
|
onChange={(value) => setFormData({ ...formData, hosting_type: value })}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Checkbox
|
|
checked={formData.is_active}
|
|
onChange={(checked) => setFormData({ ...formData, is_active: checked })}
|
|
label="Active"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Card 2: Industry & Sectors Configuration */}
|
|
<Card className="p-6 border-l-4 border-l-info-500">
|
|
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
|
<LayersIcon className="w-5 h-5 text-info-500" />
|
|
Industry & Sectors
|
|
</h3>
|
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
|
Configure up to 5 sectors for content targeting.
|
|
</p>
|
|
|
|
<div className="space-y-4">
|
|
{/* Industry Selection - Dropdown style from wizard */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Industry *
|
|
</label>
|
|
<SelectDropdown
|
|
options={industries.map(i => ({ value: i.slug, label: i.name }))}
|
|
value={selectedIndustry}
|
|
onChange={(value) => {
|
|
setSelectedIndustry(value);
|
|
setSelectedSectors([]);
|
|
}}
|
|
placeholder="Select an industry"
|
|
/>
|
|
{selectedIndustry && (
|
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
{industries.find(i => i.slug === selectedIndustry)?.description}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Sector Selection - Badge style from wizard */}
|
|
{selectedIndustry && getIndustrySectors().length > 0 && (
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Sectors * <span className="text-gray-400 font-normal">(Select up to 5)</span>
|
|
</label>
|
|
<div className="flex flex-wrap gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg max-h-48 overflow-y-auto">
|
|
{getIndustrySectors().map((sector) => {
|
|
const isSelected = selectedSectors.includes(sector.slug);
|
|
return (
|
|
<Badge
|
|
key={sector.slug}
|
|
tone={isSelected ? 'success' : 'neutral'}
|
|
variant="soft"
|
|
className={`cursor-pointer transition-all ${isSelected ? 'ring-2 ring-success-500' : 'hover:bg-gray-200 dark:hover:bg-gray-700'}`}
|
|
>
|
|
<span
|
|
onClick={() => {
|
|
if (isSelected) {
|
|
setSelectedSectors(selectedSectors.filter(s => s !== sector.slug));
|
|
} else {
|
|
if (selectedSectors.length >= 5) {
|
|
toast.error('Maximum 5 sectors allowed per site');
|
|
return;
|
|
}
|
|
setSelectedSectors([...selectedSectors, sector.slug]);
|
|
}
|
|
}}
|
|
className="flex items-center"
|
|
>
|
|
{isSelected && <CheckCircleIcon className="w-3 h-3 mr-1" />}
|
|
{sector.name}
|
|
</span>
|
|
</Badge>
|
|
);
|
|
})}
|
|
</div>
|
|
{selectedSectors.length > 0 && (
|
|
<p className="text-xs text-gray-500 mt-2">
|
|
{selectedSectors.length} sector{selectedSectors.length !== 1 ? 's' : ''} selected
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{selectedIndustry && selectedSectors.length > 0 && (
|
|
<div className="flex justify-end">
|
|
<Button
|
|
onClick={handleSelectSectors}
|
|
variant="primary"
|
|
size="sm"
|
|
disabled={isSelectingSectors}
|
|
>
|
|
{isSelectingSectors ? 'Saving...' : 'Save Sectors'}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Advanced Settings Toggle */}
|
|
<div className="mb-6">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setShowAdvancedSettings(!showAdvancedSettings)}
|
|
className="w-full justify-between"
|
|
endIcon={<ChevronDownIcon className={`w-4 h-4 transition-transform ${showAdvancedSettings ? 'rotate-180' : ''}`} />}
|
|
>
|
|
<span className="flex items-center gap-2">
|
|
<SettingsIcon className="w-4 h-4" />
|
|
Advanced Settings (SEO, Open Graph, Schema)
|
|
</span>
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Advanced Settings - 3 Column Grid */}
|
|
{showAdvancedSettings && (
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
|
{/* SEO Meta Tags */}
|
|
<Card className="p-6 border-l-4 border-l-success-500">
|
|
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
|
<DocsIcon className="w-5 h-5 text-success-500" />
|
|
SEO Meta Tags
|
|
</h3>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Label>Meta Title</Label>
|
|
<InputField
|
|
type="text"
|
|
value={formData.meta_title}
|
|
onChange={(e) => setFormData({ ...formData, meta_title: e.target.value })}
|
|
placeholder="SEO title (50-60 chars)"
|
|
max="60"
|
|
/>
|
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
{formData.meta_title.length}/60 characters
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>Meta Description</Label>
|
|
<TextArea
|
|
value={formData.meta_description}
|
|
onChange={(value) => setFormData({ ...formData, meta_description: value })}
|
|
rows={3}
|
|
placeholder="SEO description (150-160 chars)"
|
|
maxLength={160}
|
|
className="mt-1"
|
|
/>
|
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
{formData.meta_description.length}/160 characters
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>Meta Keywords</Label>
|
|
<InputField
|
|
type="text"
|
|
value={formData.meta_keywords}
|
|
onChange={(e) => setFormData({ ...formData, meta_keywords: e.target.value })}
|
|
placeholder="keyword1, keyword2"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Open Graph */}
|
|
<Card className="p-6 border-l-4 border-l-purple-500">
|
|
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
|
<PaperPlaneIcon className="w-5 h-5 text-purple-500" />
|
|
Open Graph
|
|
</h3>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Label>OG Title</Label>
|
|
<InputField
|
|
type="text"
|
|
value={formData.og_title}
|
|
onChange={(e) => setFormData({ ...formData, og_title: e.target.value })}
|
|
placeholder="Open Graph title"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>OG Description</Label>
|
|
<TextArea
|
|
value={formData.og_description}
|
|
onChange={(value) => setFormData({ ...formData, og_description: value })}
|
|
rows={3}
|
|
placeholder="Open Graph description"
|
|
className="mt-1"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>OG Image URL</Label>
|
|
<InputField
|
|
type="url"
|
|
value={formData.og_image}
|
|
onChange={(e) => setFormData({ ...formData, og_image: e.target.value })}
|
|
placeholder="https://example.com/image.jpg"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>OG Type</Label>
|
|
<SelectDropdown
|
|
options={[
|
|
{ value: 'website', label: 'Website' },
|
|
{ value: 'article', label: 'Article' },
|
|
{ value: 'business.business', label: 'Business' },
|
|
{ value: 'product', label: 'Product' },
|
|
]}
|
|
value={formData.og_type}
|
|
onChange={(value) => setFormData({ ...formData, og_type: value })}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>OG Site Name</Label>
|
|
<InputField
|
|
type="text"
|
|
value={formData.og_site_name}
|
|
onChange={(e) => setFormData({ ...formData, og_site_name: e.target.value })}
|
|
placeholder="Site name"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Schema.org */}
|
|
<Card className="p-6 border-l-4 border-l-warning-500">
|
|
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
|
<BoltIcon className="w-5 h-5 text-warning-500" />
|
|
Schema.org
|
|
</h3>
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Label>Schema Type</Label>
|
|
<SelectDropdown
|
|
options={[
|
|
{ value: 'Organization', label: 'Organization' },
|
|
{ value: 'LocalBusiness', label: 'Local Business' },
|
|
{ value: 'WebSite', label: 'Website' },
|
|
{ value: 'Corporation', label: 'Corporation' },
|
|
{ value: 'NGO', label: 'NGO' },
|
|
]}
|
|
value={formData.schema_type}
|
|
onChange={(value) => setFormData({ ...formData, schema_type: value })}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>Schema Name</Label>
|
|
<InputField
|
|
type="text"
|
|
value={formData.schema_name}
|
|
onChange={(e) => setFormData({ ...formData, schema_name: e.target.value })}
|
|
placeholder="Organization name"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>Schema Description</Label>
|
|
<TextArea
|
|
value={formData.schema_description}
|
|
onChange={(value) => setFormData({ ...formData, schema_description: value })}
|
|
rows={3}
|
|
placeholder="Description"
|
|
className="mt-1"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>Schema URL</Label>
|
|
<InputField
|
|
type="url"
|
|
value={formData.schema_url}
|
|
onChange={(e) => setFormData({ ...formData, schema_url: e.target.value })}
|
|
placeholder="https://example.com"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>Schema Logo URL</Label>
|
|
<InputField
|
|
type="url"
|
|
value={formData.schema_logo}
|
|
onChange={(e) => setFormData({ ...formData, schema_logo: e.target.value })}
|
|
placeholder="https://example.com/logo.png"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>Same As URLs</Label>
|
|
<InputField
|
|
type="text"
|
|
value={formData.schema_same_as}
|
|
onChange={(e) => setFormData({ ...formData, schema_same_as: e.target.value })}
|
|
placeholder="Social profiles (comma-separated)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
)}
|
|
|
|
{/* Save Button */}
|
|
<div className="flex justify-end">
|
|
<Button
|
|
onClick={handleSave}
|
|
variant="primary"
|
|
disabled={saving}
|
|
startIcon={saving ? <Loader2Icon className="w-4 h-4 animate-spin" /> : <SaveIcon className="w-4 h-4" />}
|
|
>
|
|
{saving ? 'Saving...' : 'Save Settings'}
|
|
</Button>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* SEO Meta Tags Tab */}
|
|
{activeTab === 'seo' && (
|
|
<Card className="p-6">
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Label>Meta Title</Label>
|
|
<InputField
|
|
type="text"
|
|
value={formData.meta_title}
|
|
onChange={(e) => setFormData({ ...formData, meta_title: e.target.value })}
|
|
placeholder="SEO title (recommended: 50-60 characters)"
|
|
max="60"
|
|
/>
|
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
{formData.meta_title.length}/60 characters
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>Meta Description</Label>
|
|
<TextArea
|
|
value={formData.meta_description}
|
|
onChange={(value) => setFormData({ ...formData, meta_description: value })}
|
|
rows={4}
|
|
placeholder="SEO description (recommended: 150-160 characters)"
|
|
maxLength={160}
|
|
className="mt-1"
|
|
/>
|
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
{formData.meta_description.length}/160 characters
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>Meta Keywords (comma-separated)</Label>
|
|
<InputField
|
|
type="text"
|
|
value={formData.meta_keywords}
|
|
onChange={(e) => setFormData({ ...formData, meta_keywords: e.target.value })}
|
|
placeholder="keyword1, keyword2, keyword3"
|
|
/>
|
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
Separate keywords with commas
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Open Graph Tab */}
|
|
{activeTab === 'og' && (
|
|
<Card className="p-6">
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Label>OG Title</Label>
|
|
<InputField
|
|
type="text"
|
|
value={formData.og_title}
|
|
onChange={(e) => setFormData({ ...formData, og_title: e.target.value })}
|
|
placeholder="Open Graph title"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>OG Description</Label>
|
|
<TextArea
|
|
value={formData.og_description}
|
|
onChange={(value) => setFormData({ ...formData, og_description: value })}
|
|
rows={4}
|
|
placeholder="Open Graph description"
|
|
className="mt-1"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>OG Image URL</Label>
|
|
<InputField
|
|
type="url"
|
|
value={formData.og_image}
|
|
onChange={(e) => setFormData({ ...formData, og_image: e.target.value })}
|
|
placeholder="https://example.com/image.jpg"
|
|
/>
|
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
Recommended: 1200x630px image
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>OG Type</Label>
|
|
<SelectDropdown
|
|
options={[
|
|
{ value: 'website', label: 'Website' },
|
|
{ value: 'article', label: 'Article' },
|
|
{ value: 'business.business', label: 'Business' },
|
|
{ value: 'product', label: 'Product' },
|
|
]}
|
|
value={formData.og_type}
|
|
onChange={(value) => setFormData({ ...formData, og_type: value })}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>OG Site Name</Label>
|
|
<InputField
|
|
type="text"
|
|
value={formData.og_site_name}
|
|
onChange={(e) => setFormData({ ...formData, og_site_name: e.target.value })}
|
|
placeholder="Site name for social sharing"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Schema.org Tab */}
|
|
{activeTab === 'schema' && (
|
|
<Card className="p-6">
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Label>Schema Type</Label>
|
|
<SelectDropdown
|
|
options={[
|
|
{ value: 'Organization', label: 'Organization' },
|
|
{ value: 'LocalBusiness', label: 'Local Business' },
|
|
{ value: 'WebSite', label: 'Website' },
|
|
{ value: 'Corporation', label: 'Corporation' },
|
|
{ value: 'NGO', label: 'NGO' },
|
|
]}
|
|
value={formData.schema_type}
|
|
onChange={(value) => setFormData({ ...formData, schema_type: value })}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>Schema Name</Label>
|
|
<InputField
|
|
type="text"
|
|
value={formData.schema_name}
|
|
onChange={(e) => setFormData({ ...formData, schema_name: e.target.value })}
|
|
placeholder="Organization name"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>Schema Description</Label>
|
|
<TextArea
|
|
value={formData.schema_description}
|
|
onChange={(value) => setFormData({ ...formData, schema_description: value })}
|
|
rows={3}
|
|
placeholder="Organization description"
|
|
className="mt-1"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>Schema URL</Label>
|
|
<InputField
|
|
type="url"
|
|
value={formData.schema_url}
|
|
onChange={(e) => setFormData({ ...formData, schema_url: e.target.value })}
|
|
placeholder="https://example.com"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>Schema Logo URL</Label>
|
|
<InputField
|
|
type="url"
|
|
value={formData.schema_logo}
|
|
onChange={(e) => setFormData({ ...formData, schema_logo: e.target.value })}
|
|
placeholder="https://example.com/logo.png"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>Same As URLs (comma-separated)</Label>
|
|
<InputField
|
|
type="text"
|
|
value={formData.schema_same_as}
|
|
onChange={(e) => setFormData({ ...formData, schema_same_as: e.target.value })}
|
|
placeholder="https://facebook.com/page, https://twitter.com/page"
|
|
/>
|
|
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
Social media profiles and other related URLs
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Integrations Tab */}
|
|
{activeTab === 'integrations' && siteId && (
|
|
<WordPressIntegrationForm
|
|
siteId={Number(siteId)}
|
|
integration={wordPressIntegration}
|
|
siteName={site?.name}
|
|
siteUrl={site?.domain || site?.wp_url}
|
|
onIntegrationUpdate={handleIntegrationUpdate}
|
|
/>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
</>
|
|
);
|
|
}
|
|
|