SEction 2 part 2
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
* Phase 7: Advanced Site Management
|
||||
* Features: SEO (meta tags, Open Graph, schema.org), Industry & Sectors Configuration
|
||||
*/
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
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';
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
} from '../../services/api';
|
||||
import WordPressIntegrationForm from '../../components/sites/WordPressIntegrationForm';
|
||||
import { integrationApi, SiteIntegration } from '../../services/integration.api';
|
||||
import { GridIcon, PlugInIcon, PaperPlaneIcon, DocsIcon, BoltIcon, FileIcon, ChevronDownIcon, CloseIcon, PlusIcon, RefreshCwIcon } from '../../icons';
|
||||
import { GridIcon, PlugInIcon, PaperPlaneIcon, DocsIcon, BoltIcon, FileIcon, ChevronDownIcon, CloseIcon, PlusIcon, RefreshCwIcon, FileTextIcon, ImageIcon, SaveIcon, Loader2Icon } from '../../icons';
|
||||
import Badge from '../../components/ui/badge/Badge';
|
||||
import { Dropdown } from '../../components/ui/dropdown/Dropdown';
|
||||
import { DropdownItem } from '../../components/ui/dropdown/DropdownItem';
|
||||
@@ -49,9 +49,9 @@ export default function SiteSettings() {
|
||||
const [isSiteSelectorOpen, setIsSiteSelectorOpen] = useState(false);
|
||||
const siteSelectorRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
// Check for tab parameter in URL
|
||||
const initialTab = (searchParams.get('tab') as 'general' | 'integrations' | 'publishing' | 'content-types') || 'general';
|
||||
const [activeTab, setActiveTab] = useState<'general' | 'integrations' | 'publishing' | 'content-types'>(initialTab);
|
||||
// Check for tab parameter in URL - now includes content-generation and image-settings tabs
|
||||
const initialTab = (searchParams.get('tab') as 'general' | 'content-generation' | 'image-settings' | 'integrations' | 'publishing' | 'content-types') || 'general';
|
||||
const [activeTab, setActiveTab] = useState<'general' | 'content-generation' | 'image-settings' | 'integrations' | 'publishing' | 'content-types'>(initialTab);
|
||||
const [contentTypes, setContentTypes] = useState<any>(null);
|
||||
const [contentTypesLoading, setContentTypesLoading] = useState(false);
|
||||
|
||||
@@ -60,6 +60,79 @@ export default function SiteSettings() {
|
||||
const [publishingSettingsLoading, setPublishingSettingsLoading] = useState(false);
|
||||
const [publishingSettingsSaving, setPublishingSettingsSaving] = useState(false);
|
||||
|
||||
// Content Generation Settings state
|
||||
const [contentGenerationSettings, setContentGenerationSettings] = useState({
|
||||
appendToPrompt: '',
|
||||
defaultTone: 'professional',
|
||||
defaultLength: 'medium',
|
||||
});
|
||||
const [contentGenerationLoading, setContentGenerationLoading] = useState(false);
|
||||
const [contentGenerationSaving, setContentGenerationSaving] = useState(false);
|
||||
|
||||
// Image Settings state
|
||||
const [imageQuality, setImageQuality] = useState<'standard' | 'premium' | 'best'>('premium');
|
||||
const [imageSettings, setImageSettings] = useState({
|
||||
enabled: true,
|
||||
service: 'openai' as 'openai' | 'runware',
|
||||
provider: 'openai',
|
||||
model: 'dall-e-3',
|
||||
image_type: 'realistic' as 'realistic' | 'artistic' | 'cartoon',
|
||||
max_in_article_images: 2,
|
||||
image_format: 'webp' as 'webp' | 'jpg' | 'png',
|
||||
desktop_enabled: true,
|
||||
mobile_enabled: true,
|
||||
featured_image_size: '1024x1024',
|
||||
desktop_image_size: '1024x1024',
|
||||
});
|
||||
const [imageSettingsLoading, setImageSettingsLoading] = useState(false);
|
||||
const [imageSettingsSaving, setImageSettingsSaving] = useState(false);
|
||||
|
||||
// Image quality to config mapping
|
||||
const QUALITY_TO_CONFIG: Record<string, { service: 'openai' | 'runware'; model: string }> = {
|
||||
standard: { service: 'openai', model: 'dall-e-2' },
|
||||
premium: { service: 'openai', model: 'dall-e-3' },
|
||||
best: { service: 'runware', model: 'runware:97@1' },
|
||||
};
|
||||
|
||||
const getQualityFromConfig = (service?: string, model?: string): 'standard' | 'premium' | 'best' => {
|
||||
if (service === 'runware') return 'best';
|
||||
if (model === 'dall-e-3') return 'premium';
|
||||
return 'standard';
|
||||
};
|
||||
|
||||
const getImageSizes = (provider: string, model: string) => {
|
||||
if (provider === 'runware') {
|
||||
return [
|
||||
{ value: '1280x832', label: '1280×832 pixels' },
|
||||
{ value: '1024x1024', label: '1024×1024 pixels' },
|
||||
{ value: '512x512', label: '512×512 pixels' },
|
||||
];
|
||||
} else if (provider === 'openai') {
|
||||
if (model === 'dall-e-2') {
|
||||
return [
|
||||
{ value: '256x256', label: '256×256 pixels' },
|
||||
{ value: '512x512', label: '512×512 pixels' },
|
||||
{ value: '1024x1024', label: '1024×1024 pixels' },
|
||||
];
|
||||
} else if (model === 'dall-e-3') {
|
||||
return [
|
||||
{ value: '1024x1024', label: '1024×1024 pixels' },
|
||||
];
|
||||
}
|
||||
}
|
||||
return [{ value: '1024x1024', label: '1024×1024 pixels' }];
|
||||
};
|
||||
|
||||
const getCurrentImageConfig = useCallback(() => {
|
||||
const config = QUALITY_TO_CONFIG[imageQuality];
|
||||
return { service: config.service, model: config.model };
|
||||
}, [imageQuality]);
|
||||
|
||||
const availableImageSizes = getImageSizes(
|
||||
getCurrentImageConfig().service,
|
||||
getCurrentImageConfig().model
|
||||
);
|
||||
|
||||
// Sectors selection state
|
||||
const [industries, setIndustries] = useState<Industry[]>([]);
|
||||
const [selectedIndustry, setSelectedIndustry] = useState<string>('');
|
||||
@@ -111,7 +184,7 @@ export default function SiteSettings() {
|
||||
useEffect(() => {
|
||||
// Update tab if URL parameter changes
|
||||
const tab = searchParams.get('tab');
|
||||
if (tab && ['general', 'integrations', 'publishing', 'content-types'].includes(tab)) {
|
||||
if (tab && ['general', 'content-generation', 'image-settings', 'integrations', 'publishing', 'content-types'].includes(tab)) {
|
||||
setActiveTab(tab as typeof activeTab);
|
||||
}
|
||||
}, [searchParams]);
|
||||
@@ -128,6 +201,49 @@ export default function SiteSettings() {
|
||||
}
|
||||
}, [activeTab, siteId]);
|
||||
|
||||
// Load content generation settings when tab is active
|
||||
useEffect(() => {
|
||||
if (activeTab === 'content-generation' && siteId) {
|
||||
loadContentGenerationSettings();
|
||||
}
|
||||
}, [activeTab, siteId]);
|
||||
|
||||
// Load image settings when tab is active
|
||||
useEffect(() => {
|
||||
if (activeTab === 'image-settings' && siteId) {
|
||||
loadImageSettings();
|
||||
}
|
||||
}, [activeTab, siteId]);
|
||||
|
||||
// Update image sizes when quality changes
|
||||
useEffect(() => {
|
||||
const config = getCurrentImageConfig();
|
||||
const sizes = getImageSizes(config.service, config.model);
|
||||
const defaultSize = sizes.length > 0 ? sizes[0].value : '1024x1024';
|
||||
|
||||
const validSizes = sizes.map(s => s.value);
|
||||
const needsFeaturedUpdate = !validSizes.includes(imageSettings.featured_image_size);
|
||||
const needsDesktopUpdate = !validSizes.includes(imageSettings.desktop_image_size);
|
||||
|
||||
if (needsFeaturedUpdate || needsDesktopUpdate) {
|
||||
setImageSettings(prev => ({
|
||||
...prev,
|
||||
service: config.service,
|
||||
provider: config.service,
|
||||
model: config.model,
|
||||
featured_image_size: needsFeaturedUpdate ? defaultSize : prev.featured_image_size,
|
||||
desktop_image_size: needsDesktopUpdate ? defaultSize : prev.desktop_image_size,
|
||||
}));
|
||||
} else {
|
||||
setImageSettings(prev => ({
|
||||
...prev,
|
||||
service: config.service,
|
||||
provider: config.service,
|
||||
model: config.model,
|
||||
}));
|
||||
}
|
||||
}, [imageQuality, getCurrentImageConfig]);
|
||||
|
||||
// Load sites for selector
|
||||
useEffect(() => {
|
||||
loadSites();
|
||||
@@ -253,6 +369,109 @@ export default function SiteSettings() {
|
||||
}
|
||||
};
|
||||
|
||||
// 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);
|
||||
}
|
||||
};
|
||||
|
||||
// Image Settings
|
||||
const loadImageSettings = async () => {
|
||||
try {
|
||||
setImageSettingsLoading(true);
|
||||
const imageData = await fetchAPI('/v1/system/settings/integrations/image_generation/');
|
||||
if (imageData) {
|
||||
const quality = getQualityFromConfig(imageData.service || imageData.provider, imageData.model);
|
||||
setImageQuality(quality);
|
||||
|
||||
setImageSettings({
|
||||
enabled: imageData.enabled !== false,
|
||||
service: imageData.service || imageData.provider || 'openai',
|
||||
provider: imageData.provider || imageData.service || 'openai',
|
||||
model: imageData.model || 'dall-e-3',
|
||||
image_type: imageData.image_type || 'realistic',
|
||||
max_in_article_images: imageData.max_in_article_images || 2,
|
||||
image_format: imageData.image_format || 'webp',
|
||||
desktop_enabled: imageData.desktop_enabled !== false,
|
||||
mobile_enabled: imageData.mobile_enabled !== false,
|
||||
featured_image_size: imageData.featured_image_size || '1024x1024',
|
||||
desktop_image_size: imageData.desktop_image_size || '1024x1024',
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Error loading image settings:', error);
|
||||
} finally {
|
||||
setImageSettingsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const saveImageSettings = async () => {
|
||||
try {
|
||||
setImageSettingsSaving(true);
|
||||
const config = getCurrentImageConfig();
|
||||
const configToSave = {
|
||||
enabled: imageSettings.enabled,
|
||||
service: config.service,
|
||||
provider: config.service,
|
||||
model: config.model,
|
||||
runwareModel: config.service === 'runware' ? config.model : undefined,
|
||||
image_type: imageSettings.image_type,
|
||||
max_in_article_images: imageSettings.max_in_article_images,
|
||||
image_format: imageSettings.image_format,
|
||||
desktop_enabled: imageSettings.desktop_enabled,
|
||||
mobile_enabled: imageSettings.mobile_enabled,
|
||||
featured_image_size: imageSettings.featured_image_size,
|
||||
desktop_image_size: imageSettings.desktop_image_size,
|
||||
};
|
||||
|
||||
await fetchAPI('/v1/system/settings/integrations/image_generation/save/', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(configToSave),
|
||||
});
|
||||
toast.success('Image settings saved successfully');
|
||||
} catch (error: any) {
|
||||
console.error('Error saving image settings:', error);
|
||||
toast.error(`Failed to save settings: ${error.message}`);
|
||||
} finally {
|
||||
setImageSettingsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadIndustries = async () => {
|
||||
try {
|
||||
const response = await fetchIndustries();
|
||||
@@ -609,14 +828,14 @@ export default function SiteSettings() {
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="mb-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex 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 ${
|
||||
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'
|
||||
@@ -625,13 +844,43 @@ export default function SiteSettings() {
|
||||
>
|
||||
General
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setActiveTab('content-generation');
|
||||
navigate(`/sites/${siteId}/settings?tab=content-generation`, { replace: true });
|
||||
}}
|
||||
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors whitespace-nowrap ${
|
||||
activeTab === 'content-generation'
|
||||
? '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={<FileTextIcon className="w-4 h-4" />}
|
||||
>
|
||||
Content
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setActiveTab('image-settings');
|
||||
navigate(`/sites/${siteId}/settings?tab=image-settings`, { replace: true });
|
||||
}}
|
||||
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors whitespace-nowrap ${
|
||||
activeTab === 'image-settings'
|
||||
? '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={<ImageIcon className="w-4 h-4" />}
|
||||
>
|
||||
Images
|
||||
</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 ${
|
||||
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors whitespace-nowrap ${
|
||||
activeTab === 'integrations'
|
||||
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
||||
@@ -646,7 +895,7 @@ export default function SiteSettings() {
|
||||
setActiveTab('publishing');
|
||||
navigate(`/sites/${siteId}/settings?tab=publishing`, { replace: true });
|
||||
}}
|
||||
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors ${
|
||||
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors whitespace-nowrap ${
|
||||
activeTab === 'publishing'
|
||||
? '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'
|
||||
@@ -662,7 +911,7 @@ export default function SiteSettings() {
|
||||
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 ${
|
||||
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors whitespace-nowrap ${
|
||||
activeTab === 'content-types'
|
||||
? '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'
|
||||
@@ -675,6 +924,241 @@ export default function SiteSettings() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Generation Tab */}
|
||||
{activeTab === 'content-generation' && (
|
||||
<div className="space-y-6 max-w-4xl">
|
||||
<Card className="p-6">
|
||||
<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 Generation</h2>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Customize how your articles are written</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-6">
|
||||
<div>
|
||||
<Label className="mb-2">Append to Every Prompt</Label>
|
||||
<TextArea
|
||||
value={contentGenerationSettings.appendToPrompt}
|
||||
onChange={(value) => setContentGenerationSettings({ ...contentGenerationSettings, appendToPrompt: value })}
|
||||
placeholder="Add custom instructions that will be included with every content generation request..."
|
||||
rows={5}
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
This text will be appended to every AI prompt. Use it to enforce brand guidelines, tone, or specific requirements.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label className="mb-2">Default Writing 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">Default Article Length</Label>
|
||||
<SelectDropdown
|
||||
options={[
|
||||
{ value: 'short', label: 'Short (500-800 words)' },
|
||||
{ value: 'medium', label: 'Medium (1000-1500 words)' },
|
||||
{ value: 'long', label: 'Long (2000-3000 words)' },
|
||||
{ value: 'comprehensive', label: 'Comprehensive (3000+ words)' },
|
||||
]}
|
||||
value={contentGenerationSettings.defaultLength}
|
||||
onChange={(value) => setContentGenerationSettings({ ...contentGenerationSettings, defaultLength: value })}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
variant="primary"
|
||||
tone="brand"
|
||||
onClick={saveContentGenerationSettings}
|
||||
disabled={contentGenerationSaving}
|
||||
startIcon={contentGenerationSaving ? <Loader2Icon className="w-4 h-4 animate-spin" /> : <SaveIcon className="w-4 h-4" />}
|
||||
>
|
||||
{contentGenerationSaving ? 'Saving...' : 'Save Settings'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Image Settings Tab */}
|
||||
{activeTab === 'image-settings' && (
|
||||
<div className="space-y-6 max-w-4xl">
|
||||
<Card className="p-6">
|
||||
<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">Configure how images are created for your articles</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{imageSettingsLoading ? (
|
||||
<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-6">
|
||||
{/* Row 1: Image Quality & Style */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label className="mb-2">Image Quality</Label>
|
||||
<SelectDropdown
|
||||
options={[
|
||||
{ value: 'standard', label: 'Standard - Fast & economical (DALL·E 2)' },
|
||||
{ value: 'premium', label: 'Premium - High quality (DALL·E 3)' },
|
||||
{ value: 'best', label: 'Best - Highest quality (Runware)' },
|
||||
]}
|
||||
value={imageQuality}
|
||||
onChange={(value) => setImageQuality(value as 'standard' | 'premium' | 'best')}
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">Higher quality produces better images</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="mb-2">Image Style</Label>
|
||||
<SelectDropdown
|
||||
options={[
|
||||
{ value: 'realistic', label: 'Realistic' },
|
||||
{ value: 'artistic', label: 'Artistic' },
|
||||
{ value: 'cartoon', label: 'Cartoon' },
|
||||
]}
|
||||
value={imageSettings.image_type}
|
||||
onChange={(value) => setImageSettings({ ...imageSettings, image_type: value as any })}
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">Choose the visual style that matches your brand</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 2: Featured Image Size */}
|
||||
<div>
|
||||
<Label className="mb-2">Featured Image</Label>
|
||||
<div className="p-4 rounded-lg border border-gray-200 dark:border-gray-700 bg-gradient-to-r from-purple-500 to-brand-500 text-white">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="font-medium">Featured Image Size</div>
|
||||
<div className="text-xs bg-white/20 px-2 py-1 rounded">Always Enabled</div>
|
||||
</div>
|
||||
<SelectDropdown
|
||||
options={availableImageSizes}
|
||||
value={imageSettings.featured_image_size}
|
||||
onChange={(value) => setImageSettings({ ...imageSettings, featured_image_size: value })}
|
||||
className="w-full [&_.igny8-select-styled]:bg-white/10 [&_.igny8-select-styled]:border-white/20 [&_.igny8-select-styled]:text-white"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 3: Desktop & Mobile Images */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="p-4 rounded-lg border border-gray-200 dark:border-gray-700 space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox
|
||||
checked={imageSettings.desktop_enabled}
|
||||
onChange={(checked) => setImageSettings({ ...imageSettings, desktop_enabled: checked })}
|
||||
/>
|
||||
<Label className="font-medium text-gray-700 dark:text-gray-300">Desktop Images</Label>
|
||||
</div>
|
||||
{imageSettings.desktop_enabled && (
|
||||
<SelectDropdown
|
||||
options={availableImageSizes}
|
||||
value={imageSettings.desktop_image_size}
|
||||
onChange={(value) => setImageSettings({ ...imageSettings, desktop_image_size: value })}
|
||||
className="w-full"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<Checkbox
|
||||
checked={imageSettings.mobile_enabled}
|
||||
onChange={(checked) => setImageSettings({ ...imageSettings, mobile_enabled: checked })}
|
||||
/>
|
||||
<div>
|
||||
<Label className="font-medium text-gray-700 dark:text-gray-300">Mobile Images</Label>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">512×512 pixels</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Row 4: Max Images & Format */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label className="mb-2">Max In-Article Images</Label>
|
||||
<SelectDropdown
|
||||
options={[
|
||||
{ value: '1', label: '1 Image' },
|
||||
{ value: '2', label: '2 Images' },
|
||||
{ value: '3', label: '3 Images' },
|
||||
{ value: '4', label: '4 Images' },
|
||||
{ value: '5', label: '5 Images' },
|
||||
]}
|
||||
value={String(imageSettings.max_in_article_images)}
|
||||
onChange={(value) => setImageSettings({ ...imageSettings, max_in_article_images: parseInt(value) })}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="mb-2">Image Format</Label>
|
||||
<SelectDropdown
|
||||
options={[
|
||||
{ value: 'webp', label: 'WEBP (recommended)' },
|
||||
{ value: 'jpg', label: 'JPG' },
|
||||
{ value: 'png', label: 'PNG' },
|
||||
]}
|
||||
value={imageSettings.image_format}
|
||||
onChange={(value) => setImageSettings({ ...imageSettings, image_format: value as any })}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
variant="primary"
|
||||
tone="brand"
|
||||
onClick={saveImageSettings}
|
||||
disabled={imageSettingsSaving}
|
||||
startIcon={imageSettingsSaving ? <Loader2Icon className="w-4 h-4 animate-spin" /> : <SaveIcon className="w-4 h-4" />}
|
||||
>
|
||||
{imageSettingsSaving ? 'Saving...' : 'Save Settings'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Publishing Tab */}
|
||||
{activeTab === 'publishing' && (
|
||||
<Card>
|
||||
|
||||
Reference in New Issue
Block a user