diff --git a/PRICING-SIMPLIFICATION-ANALYSIS.md b/PRICING-SIMPLIFICATION-ANALYSIS.md deleted file mode 100644 index bbe03753..00000000 --- a/PRICING-SIMPLIFICATION-ANALYSIS.md +++ /dev/null @@ -1,179 +0,0 @@ -# IGNY8 Pricing System - Final Plan - -**Date:** December 26, 2025 -**Status:** APPROVED - ---- - -## 1. Pricing Foundation - -### 1.1 Credit Pricing - -| Item | User Pays | -|------|-----------| -| **1 Credit** | **$0.10** | -| 1 Basic Image | 1 credit ($0.10) | -| 1 Premium Image | 4 credits ($0.40) | - -*Everything is credits. Images consume credits.* - -### 1.2 Our Costs - -| Item | Our Cost | User Pays | Margin | -|------|----------|-----------|--------| -| 1 Credit (AI tokens) | $0.01 | $0.10 | 90% | -| 1 Basic Image | $0.02 | $0.10 | 80% | -| 1 Premium Image | $0.08 | $0.40 | 80% | - ---- - -## 2. Plan Structure - -### 2.1 Credits per Plan - -| Plan | Total Credits | Image Allocation | AI Credits | Plan Price | -|------|---------------|------------------|------------|------------| -| **Starter** | 1,000 | 200 basic (or 50 premium) | 800 | **$100** | -| **Growth** | 2,000 | 800 basic (or 200 premium) | 1,200 | **$200** | -| **Scale** | 5,000 | 2,000 basic (or 500 premium) | 3,000 | **$500** | - -*Price = Total Credits × $0.10* - -### 2.2 Other Plan Limits (from screenshot) - -| Plan | Price | Sites | Users | Keywords | Articles* | -|------|-------|-------|-------|----------|-----------| -| **Starter** | $100 | 2 | 2 | 500 | ~50 | -| **Growth** | $200 | 5 | 3 | 2,000 | ~200 | -| **Scale** | $500 | 99 | 5 | 5,000 | ~500 | - -*Approximate based on credit allocation - ---- - -## 3. Credit Cost per Operation - -| Operation | Credits | Notes | -|-----------|---------|-------| -| Clustering | 5 | Per cluster batch | -| Idea Generation | 10 | Per idea | -| Content Writing (Short ~800w) | 20 | Minimum | -| Content Writing (Medium ~1500w) | 30 | Average | -| Content Writing (Long ~2500w) | 40 | Long-form | -| Image Prompt Extraction | 10 | Per article | -| Basic Image | 1 | Per image | -| Premium Image | 4 | Per image | -| Linking | 3 | Per article | -| Optimization | 5 | Per article | - ---- - -## 4. Use Case Scenarios (Full Plan Consumption) - -### 4.1 Starter Plan (1000 credits) - -| Scenario | Clustering | Ideas | Content | Img Prompts | Images | Link/Opt | Total | Fits? | -|----------|------------|-------|---------|-------------|--------|----------|-------|-------| -| **Heavy Images** | 5×5=25 | 10×10=100 | 10×25=250 | 10×10=100 | 200×1=200 | 10×8=80 | **755** | ✅ | -| **Premium Images** | 5×5=25 | 10×10=100 | 10×25=250 | 10×10=100 | 50×4=200 | 10×8=80 | **755** | ✅ | -| **Content Heavy** | 10×5=50 | 30×10=300 | 20×30=600 | 0 | 0 | 0 | **950** | ✅ | -| **Balanced** | 5×5=25 | 15×10=150 | 15×25=375 | 15×10=150 | 100×1=100 | 15×8=120 | **920** | ✅ | -| **Long Articles** | 3×5=15 | 8×10=80 | 8×40=320 | 8×10=80 | 150×1=150 | 8×8=64 | **709** | ✅ | - -**Starter delivers: 8-20 articles depending on usage pattern** - -### 4.2 Growth Plan (2000 credits) - -| Scenario | Clustering | Ideas | Content | Img Prompts | Images | Link/Opt | Total | Fits? | -|----------|------------|-------|---------|-------------|--------|----------|-------|-------| -| **Heavy Images** | 10×5=50 | 25×10=250 | 25×25=625 | 25×10=250 | 500×1=500 | 25×8=200 | **1875** | ✅ | -| **Premium Focus** | 10×5=50 | 30×10=300 | 30×25=750 | 30×10=300 | 100×4=400 | 30×8=240 | **2040** | ⚠️ | -| **Content Heavy** | 15×5=75 | 50×10=500 | 40×30=1200 | 0 | 0 | 0 | **1775** | ✅ | -| **Balanced** | 10×5=50 | 40×10=400 | 35×25=875 | 35×10=350 | 250×1=250 | 0 | **1925** | ✅ | -| **Agency Mix** | 15×5=75 | 60×10=600 | 30×30=900 | 30×10=300 | 100×1=100 | 0 | **1975** | ✅ | - -**Growth delivers: 25-50 articles depending on usage pattern** - -### 4.3 Scale Plan (5000 credits) - -| Scenario | Clustering | Ideas | Content | Img Prompts | Images | Link/Opt | Total | Fits? | -|----------|------------|-------|---------|-------------|--------|----------|-------|-------| -| **Heavy Images** | 20×5=100 | 80×10=800 | 80×25=2000 | 80×10=800 | 800×1=800 | 80×8=640 | **5140** | ⚠️ | -| **Premium Focus** | 25×5=125 | 100×10=1000 | 100×25=2500 | 100×10=1000 | 100×4=400 | 0 | **5025** | ⚠️ | -| **Content Heavy** | 30×5=150 | 150×10=1500 | 100×30=3000 | 0 | 0 | 0 | **4650** | ✅ | -| **Balanced** | 25×5=125 | 100×10=1000 | 80×30=2400 | 80×10=800 | 400×1=400 | 0 | **4725** | ✅ | -| **Full Pipeline** | 20×5=100 | 80×10=800 | 70×30=2100 | 70×10=700 | 600×1=600 | 70×8=560 | **4860** | ✅ | - -**Scale delivers: 70-150 articles depending on usage pattern** - ---- - -## 5. Cost vs Pricing Analysis - -### 5.1 Our Cost per Plan (Worst Case - Max Images) - -| Plan | AI Credits Cost | Image Cost | Total Our Cost | Plan Price | Margin | -|------|-----------------|------------|----------------|------------|--------| -| **Starter** | 800 × $0.01 = $8 | 200 × $0.02 = $4 | **$12** | $100 | **88%** | -| **Growth** | 1200 × $0.01 = $12 | 800 × $0.02 = $16 | **$28** | $200 | **86%** | -| **Scale** | 3000 × $0.01 = $30 | 2000 × $0.02 = $40 | **$70** | $500 | **86%** | - -### 5.2 Our Cost per Plan (Premium Images) - -| Plan | AI Credits Cost | Image Cost | Total Our Cost | Plan Price | Margin | -|------|-----------------|------------|----------------|------------|--------| -| **Starter** | 800 × $0.01 = $8 | 50 × $0.08 = $4 | **$12** | $100 | **88%** | -| **Growth** | 1200 × $0.01 = $12 | 200 × $0.08 = $16 | **$28** | $200 | **86%** | -| **Scale** | 3000 × $0.01 = $30 | 500 × $0.08 = $40 | **$70** | $500 | **86%** | - -### 5.3 Cost per Article - -| Plan | Plan Price | ~Articles | Price/Article | -|------|------------|-----------|---------------| -| **Starter** | $100 | 15 | **$6.67** | -| **Growth** | $200 | 40 | **$5.00** | -| **Scale** | $500 | 100 | **$5.00** | - ---- - -## 6. Final Plan Summary - -| Plan | Price | Credits | Image Allocation | Sites | Users | Keywords | -|------|-------|---------|------------------|-------|-------|----------| -| **Starter** | **$100/mo** | 1,000 | 200 basic / 50 premium | 2 | 2 | 500 | -| **Growth** | **$200/mo** | 2,000 | 800 basic / 200 premium | 5 | 3 | 2,000 | -| **Scale** | **$500/mo** | 5,000 | 2,000 basic / 500 premium | 99 | 5 | 5,000 | - -### What User Gets - -| Metric | Starter | Growth | Scale | -|--------|---------|--------|-------| -| **~Articles/month** | 15 | 40 | 100 | -| **~Images/month** | 200 | 800 | 2,000 | -| **Our Margin** | 88% | 86% | 86% | - ---- - -## 7. Simplified System - -### 7.1 Remove All Monthly Limits - -**Keep only:** -- ✅ Credits (single pool) -- ✅ Keywords (storage limit) -- ✅ Sites, Users (account limits) - -**Remove:** -- ❌ max_content_ideas -- ❌ max_content_words -- ❌ max_images_basic / max_images_premium -- ❌ max_image_prompts -- ❌ All usage tracking fields - -### 7.2 How Images Work - -Images just consume credits from the same pool: -- Basic image = 1 credit -- Premium image = 4 credits - -User decides how to spend their credits (AI or images). diff --git a/backend/igny8_core/modules/system/settings_views.py b/backend/igny8_core/modules/system/settings_views.py index db4e68f1..a7b69212 100644 --- a/backend/igny8_core/modules/system/settings_views.py +++ b/backend/igny8_core/modules/system/settings_views.py @@ -132,6 +132,124 @@ class AccountSettingsViewSet(AccountModelViewSet): serializer.save(account=account) +@extend_schema_view( + retrieve=extend_schema(tags=['Content Settings']), + update=extend_schema(tags=['Content Settings']), +) +class ContentSettingsViewSet(viewsets.ViewSet): + """ + ViewSet for managing Content Generation and Publishing settings. + Uses AccountSettings model with specific keys: + - 'content_generation': append_to_prompt, default_tone, default_length + - 'publishing': auto_publish_enabled, auto_sync_enabled + """ + permission_classes = [IsAuthenticatedAndActive, HasTenantAccess] + authentication_classes = [JWTAuthentication] + throttle_scope = 'system' + throttle_classes = [DebugScopedRateThrottle] + + def _get_account(self, request): + """Get account from request""" + account = getattr(request, 'account', None) + if not account: + user = getattr(request, 'user', None) + if user: + account = getattr(user, 'account', None) + return account + + def retrieve(self, request, pk=None): + """Get content settings by key (content_generation or publishing)""" + account = self._get_account(request) + if not account: + return error_response( + error='Account is required', + status_code=status.HTTP_400_BAD_REQUEST, + request=request + ) + + # Validate key + if pk not in ['content_generation', 'publishing']: + return error_response( + error='Invalid settings key. Use "content_generation" or "publishing".', + status_code=status.HTTP_400_BAD_REQUEST, + request=request + ) + + try: + setting = AccountSettings.objects.get(account=account, key=pk) + return success_response(data={ + 'key': setting.key, + 'config': setting.config, + 'is_active': setting.is_active, + }, request=request) + except AccountSettings.DoesNotExist: + # Return default settings if not yet saved + if pk == 'content_generation': + default_config = { + 'append_to_prompt': '', + 'default_tone': 'professional', + 'default_length': 'medium', + } + else: # publishing + default_config = { + 'auto_publish_enabled': False, + 'auto_sync_enabled': False, + } + return success_response(data={ + 'key': pk, + 'config': default_config, + 'is_active': True, + }, request=request) + + def update(self, request, pk=None): + """Update content settings by key""" + account = self._get_account(request) + if not account: + return error_response( + error='Account is required', + status_code=status.HTTP_400_BAD_REQUEST, + request=request + ) + + # Validate key + if pk not in ['content_generation', 'publishing']: + return error_response( + error='Invalid settings key. Use "content_generation" or "publishing".', + status_code=status.HTTP_400_BAD_REQUEST, + request=request + ) + + config = request.data.get('config', request.data) + + # Validate config fields + if pk == 'content_generation': + valid_fields = {'append_to_prompt', 'default_tone', 'default_length'} + else: # publishing + valid_fields = {'auto_publish_enabled', 'auto_sync_enabled'} + + # Filter to only valid fields + filtered_config = {k: v for k, v in config.items() if k in valid_fields} + + # Get or create setting + setting, created = AccountSettings.objects.update_or_create( + account=account, + key=pk, + defaults={'config': filtered_config, 'is_active': True} + ) + + return success_response(data={ + 'key': setting.key, + 'config': setting.config, + 'is_active': setting.is_active, + 'message': 'Settings saved successfully', + }, request=request) + + @action(detail=True, methods=['post'], url_path='save', url_name='save') + def save_settings(self, request, pk=None): + """Save content settings (POST endpoint for frontend compatibility)""" + return self.update(request, pk) + + @extend_schema_view( list=extend_schema(tags=['System']), create=extend_schema(tags=['System']), diff --git a/backend/igny8_core/modules/system/urls.py b/backend/igny8_core/modules/system/urls.py index aa2a08e4..df06d64d 100644 --- a/backend/igny8_core/modules/system/urls.py +++ b/backend/igny8_core/modules/system/urls.py @@ -7,7 +7,8 @@ from .views import AIPromptViewSet, AuthorProfileViewSet, StrategyViewSet, syste from .integration_views import IntegrationSettingsViewSet from .settings_views import ( SystemSettingsViewSet, AccountSettingsViewSet, UserSettingsViewSet, - ModuleSettingsViewSet, ModuleEnableSettingsViewSet, AISettingsViewSet + ModuleSettingsViewSet, ModuleEnableSettingsViewSet, AISettingsViewSet, + ContentSettingsViewSet ) router = DefaultRouter() router.register(r'prompts', AIPromptViewSet, basename='prompts') @@ -57,6 +58,15 @@ module_enable_viewset = ModuleEnableSettingsViewSet.as_view({ 'get': 'list', }) +# Content settings viewsets for Content Generation and Publishing settings +content_settings_detail_viewset = ContentSettingsViewSet.as_view({ + 'get': 'retrieve', +}) + +content_settings_save_viewset = ContentSettingsViewSet.as_view({ + 'post': 'save_settings', + 'put': 'update', +}) urlpatterns = [ # Module enable settings endpoint - MUST come before router.urls to avoid conflict # When /settings/modules/enable/ is called, it would match ModuleSettingsViewSet with pk='enable' @@ -85,5 +95,10 @@ urlpatterns = [ path('settings/integrations//save/', integration_save_viewset, name='integration-settings-save'), # GET: Retrieve settings - Base path comes last path('settings/integrations//', integration_detail_viewset, name='integration-settings-detail'), + # Content settings routes for Content Generation and Publishing + # POST/PUT: Save content settings - must come before GET + path('settings/content//save/', content_settings_save_viewset, name='content-settings-save'), + # GET: Retrieve content settings + path('settings/content//', content_settings_detail_viewset, name='content-settings-detail'), ] diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5d36310c..b530eaf2 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -93,7 +93,6 @@ const Sites = lazy(() => import("./pages/Settings/Sites")); // Sites - Lazy loaded const SiteList = lazy(() => import("./pages/Sites/List")); -const SiteManage = lazy(() => import("./pages/Sites/Manage")); const SiteDashboard = lazy(() => import("./pages/Sites/Dashboard")); const SiteContent = lazy(() => import("./pages/Sites/Content")); const PageManager = lazy(() => import("./pages/Sites/PageManager")); @@ -232,7 +231,6 @@ export default function App() { {/* Sites Management */} } /> - } /> } /> } /> } /> diff --git a/frontend/src/components/sites/SiteSetupChecklist.tsx b/frontend/src/components/sites/SiteSetupChecklist.tsx new file mode 100644 index 00000000..8cea556f --- /dev/null +++ b/frontend/src/components/sites/SiteSetupChecklist.tsx @@ -0,0 +1,191 @@ +/** + * Site Setup Checklist Component + * Displays setup progress for a site to guide users through configuration + */ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Card } from '../ui/card'; +import Button from '../ui/button/Button'; +import { CheckLineIcon } from '../../icons'; + +interface SetupItem { + id: string; + label: string; + completed: boolean; + href: string; +} + +interface SiteSetupChecklistProps { + siteId: number; + siteName: string; + hasIndustry: boolean; + hasSectors: boolean; + hasWordPressIntegration: boolean; + hasKeywords: boolean; + compact?: boolean; +} + +export default function SiteSetupChecklist({ + siteId, + siteName, + hasIndustry, + hasSectors, + hasWordPressIntegration, + hasKeywords, + compact = false, +}: SiteSetupChecklistProps) { + const navigate = useNavigate(); + + const setupItems: SetupItem[] = [ + { + id: 'created', + label: 'Site created', + completed: true, // Always true if this component is rendered + href: `/sites/${siteId}/settings`, + }, + { + id: 'industry', + label: 'Industry/Sectors selected', + completed: hasIndustry && hasSectors, + href: `/sites/${siteId}/settings`, + }, + { + id: 'wordpress', + label: 'WordPress integration configured', + completed: hasWordPressIntegration, + href: `/sites/${siteId}/settings?tab=integrations`, + }, + { + id: 'keywords', + label: 'Keywords added', + completed: hasKeywords, + href: '/setup/add-keywords', + }, + ]; + + const completedCount = setupItems.filter((item) => item.completed).length; + const totalCount = setupItems.length; + const isComplete = completedCount === totalCount; + const progressPercent = Math.round((completedCount / totalCount) * 100); + + // Find first incomplete item for "Complete Setup" button + const firstIncomplete = setupItems.find((item) => !item.completed); + + if (compact) { + // Compact version for list view + return ( +
+
+ {setupItems.map((item) => ( +
+ ))} +
+ + {completedCount}/{totalCount} + + {isComplete && ( + + ✓ Ready + + )} +
+ ); + } + + // Full version for dashboard + return ( + +
+

+ Site Setup Progress +

+ + {progressPercent}% complete + +
+ + {/* Progress bar */} +
+
+
+ + {/* Checklist items */} +
+ {setupItems.map((item) => ( + + ))} +
+ + {/* Action button */} + {isComplete ? ( +
+ + + Ready to create content! + + +
+ ) : firstIncomplete ? ( + + ) : null} + + ); +} diff --git a/frontend/src/pages/Setup/IndustriesSectorsKeywords.tsx b/frontend/src/pages/Setup/IndustriesSectorsKeywords.tsx index 76d6ef45..6520383a 100644 --- a/frontend/src/pages/Setup/IndustriesSectorsKeywords.tsx +++ b/frontend/src/pages/Setup/IndustriesSectorsKeywords.tsx @@ -6,6 +6,7 @@ */ import { useState, useEffect, useRef, useMemo, useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; import PageMeta from '../../components/common/PageMeta'; import PageHeader from '../../components/common/PageHeader'; import { useToast } from '../../components/ui/toast/ToastContainer'; @@ -60,6 +61,14 @@ export default function IndustriesSectorsKeywords() { const [searchTerm, setSearchTerm] = useState(''); const [countryFilter, setCountryFilter] = useState(''); const [difficultyFilter, setDifficultyFilter] = useState(''); + const [showNotAddedOnly, setShowNotAddedOnly] = useState(false); + + // Keyword count tracking + const [addedCount, setAddedCount] = useState(0); + const [availableCount, setAvailableCount] = useState(0); + + // Navigation + const navigate = useNavigate(); // Import modal state const [isImportModalOpen, setIsImportModalOpen] = useState(false); @@ -183,6 +192,17 @@ export default function IndustriesSectorsKeywords() { }; }); + // Calculate counts before applying filters + const totalAdded = filteredResults.filter(sk => sk.isAdded).length; + const totalAvailable = filteredResults.filter(sk => !sk.isAdded).length; + setAddedCount(totalAdded); + setAvailableCount(totalAvailable); + + // Apply "not yet added" filter + if (showNotAddedOnly) { + filteredResults = filteredResults.filter(sk => !sk.isAdded); + } + // Apply difficulty filter if (difficultyFilter) { const difficultyNum = parseInt(difficultyFilter); @@ -245,8 +265,10 @@ export default function IndustriesSectorsKeywords() { setSeedKeywords([]); setTotalCount(0); setTotalPages(1); + setAddedCount(0); + setAvailableCount(0); } - }, [activeSite, activeSector, currentPage, pageSize, searchTerm, countryFilter, difficultyFilter, sortBy, sortDirection, toast]); + }, [activeSite, activeSector, currentPage, pageSize, searchTerm, countryFilter, difficultyFilter, showNotAddedOnly, sortBy, sortDirection, toast]); // Load data on mount and when filters change useEffect(() => { @@ -598,6 +620,15 @@ export default function IndustriesSectorsKeywords() { { value: '5', label: '5 - Very Hard' }, ], }, + { + key: 'showNotAddedOnly', + label: 'Status', + type: 'select' as const, + options: [ + { value: '', label: 'All Keywords' }, + { value: 'true', label: 'Not Yet Added Only' }, + ], + }, ], bulkActions: !activeSector ? [] : [ { @@ -673,6 +704,45 @@ export default function IndustriesSectorsKeywords() {
)} + {/* Keyword Count Summary & Next Step CTA */} + {activeSite && activeSector && ( +
+
+
+
+ + {addedCount} keywords in your workflow + +
+
+
+ + {availableCount} available to add + +
+
+ {addedCount > 0 && ( + + )} +
+ {/* Coming Soon Teaser */} +

+ Looking for more keywords? Keyword Research coming soon! +

+
+ )} + { const stringValue = value === null || value === undefined ? '' : String(value); @@ -695,6 +766,9 @@ export default function IndustriesSectorsKeywords() { } else if (key === 'difficulty') { setDifficultyFilter(stringValue); setCurrentPage(1); + } else if (key === 'showNotAddedOnly') { + setShowNotAddedOnly(stringValue === 'true'); + setCurrentPage(1); } }} onBulkAction={async (actionKey: string, ids: string[]) => { diff --git a/frontend/src/pages/Sites/Dashboard.tsx b/frontend/src/pages/Sites/Dashboard.tsx index f171c148..f220ce28 100644 --- a/frontend/src/pages/Sites/Dashboard.tsx +++ b/frontend/src/pages/Sites/Dashboard.tsx @@ -10,21 +10,18 @@ import PageHeader from '../../components/common/PageHeader'; import ComponentCard from '../../components/common/ComponentCard'; import { Card } from '../../components/ui/card'; import Button from '../../components/ui/button/Button'; -import EnhancedMetricCard from '../../components/dashboard/EnhancedMetricCard'; import { useToast } from '../../components/ui/toast/ToastContainer'; -import { fetchAPI } from '../../services/api'; -// import { fetchSiteBlueprints } from '../../services/api'; -import SiteProgressWidget from '../../components/sites/SiteProgressWidget'; +import { fetchAPI, fetchSiteSectors } from '../../services/api'; +import SiteSetupChecklist from '../../components/sites/SiteSetupChecklist'; +import { integrationApi } from '../../services/integration.api'; import { - EyeIcon, FileIcon, PlugInIcon, - ArrowUpIcon, - CalendarIcon, GridIcon, BoltIcon, PageIcon, - ArrowRightIcon + ArrowRightIcon, + ArrowUpIcon } from '../../icons'; interface Site { @@ -38,19 +35,15 @@ interface Site { created_at: string; updated_at: string; domain?: string; + industry?: string; + industry_name?: string; } -interface SiteStats { - total_pages: number; - published_pages: number; - draft_pages: number; - total_content: number; - published_content: number; - integrations_count: number; - deployments_count: number; - last_deployment?: string; - views_count?: number; - visitors_count?: number; +interface SiteSetupState { + hasIndustry: boolean; + hasSectors: boolean; + hasWordPressIntegration: boolean; + hasKeywords: boolean; } export default function SiteDashboard() { @@ -58,8 +51,12 @@ export default function SiteDashboard() { const navigate = useNavigate(); const toast = useToast(); const [site, setSite] = useState(null); - const [stats, setStats] = useState(null); - const [blueprints, setBlueprints] = useState([]); + const [setupState, setSetupState] = useState({ + hasIndustry: false, + hasSectors: false, + hasWordPressIntegration: false, + hasKeywords: false, + }); const [loading, setLoading] = useState(true); useEffect(() => { @@ -71,22 +68,49 @@ export default function SiteDashboard() { const loadSiteData = async () => { try { setLoading(true); - const [siteData, statsData] = await Promise.all([ - fetchAPI(`/v1/auth/sites/${siteId}/`), - fetchSiteStats(), - // fetchSiteBlueprints({ site_id: Number(siteId) }), - ]); + // Load site data + const siteData = await fetchAPI(`/v1/auth/sites/${siteId}/`); if (siteData) { setSite(siteData); - } - - if (statsData) { - setStats(statsData); - } - - if (blueprintsData && blueprintsData.results) { - setBlueprints(blueprintsData.results); + + // Check setup state + const hasIndustry = !!siteData.industry || !!siteData.industry_name; + + // Load sectors + let hasSectors = false; + try { + const sectors = await fetchSiteSectors(Number(siteId)); + hasSectors = sectors && sectors.length > 0; + } catch (err) { + console.log('Could not load sectors'); + } + + // Check WordPress integration + let hasWordPressIntegration = false; + try { + const wpIntegration = await integrationApi.getWordPressIntegration(Number(siteId)); + hasWordPressIntegration = !!wpIntegration; + } catch (err) { + // No integration is fine + } + + // Check keywords - try to load keywords for this site + let hasKeywords = false; + try { + const { fetchKeywords } = await import('../../services/api'); + const keywordsData = await fetchKeywords({ site_id: Number(siteId), page_size: 1 }); + hasKeywords = keywordsData?.results?.length > 0 || keywordsData?.count > 0; + } catch (err) { + // No keywords is fine + } + + setSetupState({ + hasIndustry, + hasSectors, + hasWordPressIntegration, + hasKeywords, + }); } } catch (error: any) { toast.error(`Failed to load site data: ${error.message}`); @@ -95,25 +119,6 @@ export default function SiteDashboard() { } }; - const fetchSiteStats = async (): Promise => { - try { - // TODO: Create backend endpoint for site stats - // For now, return mock data structure - return { - total_pages: 0, - published_pages: 0, - draft_pages: 0, - total_content: 0, - published_content: 0, - integrations_count: 0, - deployments_count: 0, - }; - } catch (error) { - console.error('Error fetching site stats:', error); - return null; - } - }; - if (loading) { return (
@@ -139,51 +144,6 @@ export default function SiteDashboard() { ); } - const statCards = [ - { - title: 'Total Pages', - value: stats?.total_pages || 0, - icon: , - accentColor: 'blue' as const, - href: `/sites/${siteId}/pages`, - }, - { - title: 'Published Pages', - value: stats?.published_pages || 0, - icon: , - accentColor: 'success' as const, - href: `/sites/${siteId}/pages?status=published`, - }, - { - title: 'Draft Pages', - value: stats?.draft_pages || 0, - icon: , - accentColor: 'orange' as const, - href: `/sites/${siteId}/pages?status=draft`, - }, - { - title: 'Integrations', - value: stats?.integrations_count || 0, - icon: , - accentColor: 'purple' as const, - href: `/sites/${siteId}/settings?tab=integrations`, - }, - { - title: 'Deployments', - value: stats?.deployments_count || 0, - icon: , - accentColor: 'blue' as const, - href: `/sites/${siteId}/preview`, - }, - { - title: 'Total Content', - value: stats?.total_content || 0, - icon: , - accentColor: 'purple' as const, - href: `/sites/${siteId}/content`, - }, - ]; - return (
@@ -212,34 +172,18 @@ export default function SiteDashboard() {
- {/* Stage 3: Site Progress Widget */} - {blueprints.length > 0 && ( -
- {blueprints.map((blueprint) => ( - - ))} -
- )} - - {/* Stats Grid - Using EnhancedMetricCard */} -
- {statCards.map((stat, index) => ( - - ))} + {/* Site Setup Checklist */} +
+
- {/* Quick Actions */}
@@ -315,34 +259,14 @@ export default function SiteDashboard() {
- {/* Recent Activity */} - + {/* Recent Activity - Placeholder */} +

Recent Activity

-
- {stats?.last_deployment ? ( -
-
- -
-
-

- Last Deployment -

-

- {new Date(stats.last_deployment).toLocaleString()} -

-
-
- ) : ( -

- No recent activity -

- )} -
-
-
+

+ No recent activity +

); } diff --git a/frontend/src/pages/Sites/Manage.tsx b/frontend/src/pages/Sites/Manage.tsx deleted file mode 100644 index 632290d4..00000000 --- a/frontend/src/pages/Sites/Manage.tsx +++ /dev/null @@ -1,238 +0,0 @@ -/** - * Site Management Dashboard - * Phase 6: Site Integration & Multi-Destination Publishing - */ -import React, { useState, useEffect } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { PlusIcon, EditIcon, SettingsIcon, EyeIcon, TrashIcon, PlugIcon } from 'lucide-react'; -import PageMeta from '../../components/common/PageMeta'; -import { Card } from '../../components/ui/card'; -import Button from '../../components/ui/button/Button'; -import { useToast } from '../../components/ui/toast/ToastContainer'; -import { fetchAPI } from '../../services/api'; -import SiteTypeBadge from '../../components/sites/SiteTypeBadge'; -import Badge from '../../components/ui/badge/Badge'; - -interface Site { - id: number; - name: string; - slug: string; - site_type: string; - hosting_type: string; - status: string; - is_active: boolean; - created_at: string; - updated_at: string; - page_count?: number; - integration_count?: number; - has_wordpress_integration?: boolean; -} - -export default function SiteManagement() { - const navigate = useNavigate(); - const toast = useToast(); - const [sites, setSites] = useState([]); - const [loading, setLoading] = useState(true); - - useEffect(() => { - loadSites(); - }, []); - - const loadSites = async () => { - try { - setLoading(true); - const data = await fetchAPI('/v1/auth/sites/'); - if (data && Array.isArray(data)) { - // Check for WordPress integrations - const sitesWithIntegrations = await Promise.all( - data.map(async (site: Site) => { - if (site.hosting_type === 'wordpress') { - try { - const integrations = await fetchAPI(`/v1/integration/integrations/?site=${site.id}&platform=wordpress`); - return { - ...site, - has_wordpress_integration: integrations?.results?.length > 0 || integrations?.length > 0, - }; - } catch { - return { ...site, has_wordpress_integration: false }; - } - } - return site; - }) - ); - setSites(sitesWithIntegrations); - } - } catch (error: any) { - toast.error(`Failed to load sites: ${error.message}`); - } finally { - setLoading(false); - } - }; - - const handleIntegration = (siteId: number) => { - navigate(`/sites/${siteId}/settings?tab=integrations`); - }; - - const handleCreateSite = () => { - navigate('/sites/builder'); - }; - - const handleEdit = (siteId: number) => { - navigate(`/sites/${siteId}/edit`); - }; - - const handleSettings = (siteId: number) => { - navigate(`/sites/${siteId}/settings`); - }; - - const handleView = (siteId: number) => { - navigate(`/sites/${siteId}`); - }; - - const handleDelete = async (siteId: number) => { - if (!confirm('Are you sure you want to delete this site?')) return; - - try { - await fetchAPI(`/v1/auth/sites/${siteId}/`, { - method: 'DELETE', - }); - toast.success('Site deleted successfully'); - loadSites(); - } catch (error: any) { - toast.error(`Failed to delete site: ${error.message}`); - } - }; - - if (loading) { - return ( -
- -
-
Loading sites...
-
-
- ); - } - - return ( -
- - -
-
-

- Site Management -

-

- Manage your sites, pages, and content -

-
- -
- - {sites.length === 0 ? ( - -

- No sites created yet -

- -
- ) : ( -
- {sites.map((site) => ( - -
-
-
-

- {site.name} -

-

- {site.slug} -

-
- - {site.is_active ? 'Active' : 'Inactive'} - -
- -
- - - {site.site_type} - -
- - {/* WordPress Integration Button */} - {site.hosting_type === 'wordpress' && ( -
- -
- )} - -
-
- {site.page_count || 0} pages -
-
- - - - -
-
-
-
- ))} -
- )} -
- ); -} - diff --git a/frontend/src/pages/account/ContentSettingsPage.tsx b/frontend/src/pages/account/ContentSettingsPage.tsx index 362d2fee..3c69c8d7 100644 --- a/frontend/src/pages/account/ContentSettingsPage.tsx +++ b/frontend/src/pages/account/ContentSettingsPage.tsx @@ -194,8 +194,34 @@ export default function ContentSettingsPage() { }); } - // TODO: Load content generation settings when API is available - // TODO: Load publishing settings when API is available + // Load content generation settings + try { + const contentData = await fetchAPI('/v1/system/settings/content/content_generation/'); + if (contentData?.config) { + setContentSettings({ + appendToPrompt: contentData.config.append_to_prompt || '', + defaultTone: contentData.config.default_tone || 'professional', + defaultLength: contentData.config.default_length || 'medium', + }); + } + } catch (err) { + // Settings may not exist yet, use defaults + console.log('Content generation settings not found, using defaults'); + } + + // Load publishing settings + try { + const publishData = await fetchAPI('/v1/system/settings/content/publishing/'); + if (publishData?.config) { + setPublishingSettings({ + autoPublishEnabled: publishData.config.auto_publish_enabled || false, + autoSyncEnabled: publishData.config.auto_sync_enabled || false, + }); + } + } catch (err) { + // Settings may not exist yet, use defaults + console.log('Publishing settings not found, using defaults'); + } } catch (error: any) { console.error('Error loading content settings:', error); @@ -231,8 +257,32 @@ export default function ContentSettingsPage() { }); } - // TODO: Save content generation settings when API is available - // TODO: Save publishing settings when API is available + // Save content generation settings + if (activeTab === 'content') { + await fetchAPI('/v1/system/settings/content/content_generation/save/', { + method: 'POST', + body: JSON.stringify({ + config: { + append_to_prompt: contentSettings.appendToPrompt, + default_tone: contentSettings.defaultTone, + default_length: contentSettings.defaultLength, + } + }), + }); + } + + // Save publishing settings + if (activeTab === 'publishing') { + await fetchAPI('/v1/system/settings/content/publishing/save/', { + method: 'POST', + body: JSON.stringify({ + config: { + auto_publish_enabled: publishingSettings.autoPublishEnabled, + auto_sync_enabled: publishingSettings.autoSyncEnabled, + } + }), + }); + } toast.success('Settings saved successfully'); } catch (error: any) {