Section 2 COmpleted

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-27 02:20:55 +00:00
parent 890e138829
commit add04e2ad5
9 changed files with 527 additions and 574 deletions

View File

@@ -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).

View File

@@ -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']),

View File

@@ -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/<str:pk>/save/', integration_save_viewset, name='integration-settings-save'),
# GET: Retrieve settings - Base path comes last
path('settings/integrations/<str:pk>/', 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/<str:pk>/save/', content_settings_save_viewset, name='content-settings-save'),
# GET: Retrieve content settings
path('settings/content/<str:pk>/', content_settings_detail_viewset, name='content-settings-detail'),
]

View File

@@ -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 */}
<Route path="/sites" element={<SiteList />} />
<Route path="/sites/manage" element={<SiteManage />} />
<Route path="/sites/:id" element={<SiteDashboard />} />
<Route path="/sites/:id/pages" element={<PageManager />} />
<Route path="/sites/:id/pages/new" element={<PageManager />} />

View File

@@ -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 (
<div className="flex items-center gap-2">
<div className="flex items-center gap-1">
{setupItems.map((item) => (
<div
key={item.id}
className={`w-2 h-2 rounded-full ${
item.completed
? 'bg-green-500'
: 'bg-gray-300 dark:bg-gray-600'
}`}
title={item.label}
/>
))}
</div>
<span className="text-xs text-gray-500 dark:text-gray-400">
{completedCount}/{totalCount}
</span>
{isComplete && (
<span className="text-xs text-green-600 dark:text-green-400 font-medium">
Ready
</span>
)}
</div>
);
}
// Full version for dashboard
return (
<Card className="p-5">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
Site Setup Progress
</h3>
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
{progressPercent}% complete
</span>
</div>
{/* Progress bar */}
<div className="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-full mb-4">
<div
className={`h-full rounded-full transition-all duration-500 ${
isComplete ? 'bg-green-500' : 'bg-blue-500'
}`}
style={{ width: `${progressPercent}%` }}
/>
</div>
{/* Checklist items */}
<div className="space-y-2 mb-4">
{setupItems.map((item) => (
<button
key={item.id}
onClick={() => navigate(item.href)}
className="w-full flex items-center gap-3 p-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors text-left"
>
<div
className={`flex-shrink-0 w-5 h-5 rounded-full flex items-center justify-center ${
item.completed
? 'bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400'
: 'bg-gray-100 dark:bg-gray-800 text-gray-400 dark:text-gray-500'
}`}
>
{item.completed ? (
<CheckLineIcon className="w-3 h-3" />
) : (
<span className="w-2 h-2 rounded-full bg-current" />
)}
</div>
<span
className={`text-sm ${
item.completed
? 'text-gray-600 dark:text-gray-400'
: 'text-gray-900 dark:text-white font-medium'
}`}
>
{item.label}
</span>
</button>
))}
</div>
{/* Action button */}
{isComplete ? (
<div className="flex items-center gap-2 p-3 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
<CheckLineIcon className="w-5 h-5 text-green-600 dark:text-green-400" />
<span className="text-sm font-medium text-green-700 dark:text-green-300">
Ready to create content!
</span>
<Button
size="sm"
variant="primary"
className="ml-auto"
onClick={() => navigate('/planner/keywords')}
>
Start Planning
</Button>
</div>
) : firstIncomplete ? (
<Button
variant="primary"
className="w-full"
onClick={() => navigate(firstIncomplete.href)}
endIcon={
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
}
>
Complete Setup
</Button>
) : null}
</Card>
);
}

View File

@@ -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() {
</div>
)}
{/* Keyword Count Summary & Next Step CTA */}
{activeSite && activeSector && (
<div className="mx-6 mt-6 mb-4">
<div className="flex flex-wrap items-center justify-between gap-4 p-4 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm">
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
<span className="text-green-600 dark:text-green-400 font-bold">{addedCount}</span> keywords in your workflow
</span>
</div>
<div className="w-px h-5 bg-gray-300 dark:bg-gray-600 hidden sm:block" />
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
<span className="text-blue-600 dark:text-blue-400 font-bold">{availableCount}</span> available to add
</span>
</div>
</div>
{addedCount > 0 && (
<Button
variant="success"
size="sm"
onClick={() => navigate('/planner/keywords')}
endIcon={
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
}
>
Next: Plan Your Content
</Button>
)}
</div>
{/* Coming Soon Teaser */}
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2 text-center">
Looking for more keywords? Keyword Research coming soon!
</p>
</div>
)}
<TablePageTemplate
columns={pageConfig.columns}
data={seedKeywords}
@@ -683,6 +753,7 @@ export default function IndustriesSectorsKeywords() {
search: searchTerm,
country: countryFilter,
difficulty: difficultyFilter,
showNotAddedOnly: showNotAddedOnly ? 'true' : '',
}}
onFilterChange={(key, value) => {
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[]) => {

View File

@@ -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<Site | null>(null);
const [stats, setStats] = useState<SiteStats | null>(null);
const [blueprints, setBlueprints] = useState<any[]>([]);
const [setupState, setSetupState] = useState<SiteSetupState>({
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);
}
// Check setup state
const hasIndustry = !!siteData.industry || !!siteData.industry_name;
if (blueprintsData && blueprintsData.results) {
setBlueprints(blueprintsData.results);
// 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<SiteStats | null> => {
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 (
<div className="p-6">
@@ -139,51 +144,6 @@ export default function SiteDashboard() {
);
}
const statCards = [
{
title: 'Total Pages',
value: stats?.total_pages || 0,
icon: <FileIcon />,
accentColor: 'blue' as const,
href: `/sites/${siteId}/pages`,
},
{
title: 'Published Pages',
value: stats?.published_pages || 0,
icon: <GridIcon />,
accentColor: 'success' as const,
href: `/sites/${siteId}/pages?status=published`,
},
{
title: 'Draft Pages',
value: stats?.draft_pages || 0,
icon: <FileIcon />,
accentColor: 'orange' as const,
href: `/sites/${siteId}/pages?status=draft`,
},
{
title: 'Integrations',
value: stats?.integrations_count || 0,
icon: <PlugInIcon />,
accentColor: 'purple' as const,
href: `/sites/${siteId}/settings?tab=integrations`,
},
{
title: 'Deployments',
value: stats?.deployments_count || 0,
icon: <ArrowUpIcon />,
accentColor: 'blue' as const,
href: `/sites/${siteId}/preview`,
},
{
title: 'Total Content',
value: stats?.total_content || 0,
icon: <FileIcon />,
accentColor: 'purple' as const,
href: `/sites/${siteId}/content`,
},
];
return (
<div className="p-6">
<PageMeta title={`${site.name} - Dashboard`} />
@@ -212,34 +172,18 @@ export default function SiteDashboard() {
</div>
</div>
{/* Stage 3: Site Progress Widget */}
{blueprints.length > 0 && (
<div className="mb-6">
{blueprints.map((blueprint) => (
<SiteProgressWidget
key={blueprint.id}
blueprintId={blueprint.id}
siteId={Number(siteId)}
/>
))}
</div>
)}
{/* Stats Grid - Using EnhancedMetricCard */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
{statCards.map((stat, index) => (
<EnhancedMetricCard
key={index}
title={stat.title}
value={stat.value}
icon={stat.icon}
accentColor={stat.accentColor}
href={stat.href}
/>
))}
{/* Site Setup Checklist */}
<div className="mb-6">
<SiteSetupChecklist
siteId={Number(siteId)}
siteName={site.name}
hasIndustry={setupState.hasIndustry}
hasSectors={setupState.hasSectors}
hasWordPressIntegration={setupState.hasWordPressIntegration}
hasKeywords={setupState.hasKeywords}
/>
</div>
{/* Quick Actions */}
<ComponentCard title="Quick Actions" desc="Common site management tasks">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
@@ -315,34 +259,14 @@ export default function SiteDashboard() {
</div>
</ComponentCard>
{/* Recent Activity */}
<Card className="p-6">
{/* Recent Activity - Placeholder */}
<Card className="p-6 mt-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Recent Activity
</h2>
<div className="space-y-3">
{stats?.last_deployment ? (
<div className="flex items-center gap-3 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div className="flex-shrink-0 size-10 rounded-lg bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] flex items-center justify-center text-white shadow-md">
<CalendarIcon className="h-5 w-5" />
</div>
<div>
<p className="text-sm font-medium text-gray-900 dark:text-white">
Last Deployment
</p>
<p className="text-xs text-gray-600 dark:text-gray-400">
{new Date(stats.last_deployment).toLocaleString()}
</p>
</div>
</div>
) : (
<p className="text-sm text-gray-600 dark:text-gray-400">
No recent activity
</p>
)}
</div>
</Card>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400">
No recent activity
</p>
);
}

View File

@@ -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<Site[]>([]);
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 (
<div className="p-6">
<PageMeta title="Site Management" />
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading sites...</div>
</div>
</div>
);
}
return (
<div className="p-6">
<PageMeta title="Site Management - IGNY8" />
<div className="mb-6 flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Site Management
</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Manage your sites, pages, and content
</p>
</div>
<Button onClick={handleCreateSite} variant="primary" startIcon={<PlusIcon className="w-4 h-4" />}>
Create New Site
</Button>
</div>
{sites.length === 0 ? (
<Card className="p-12 text-center">
<p className="text-gray-600 dark:text-gray-400 mb-4">
No sites created yet
</p>
<Button onClick={handleCreateSite} variant="primary">
Create Your First Site
</Button>
</Card>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{sites.map((site) => (
<Card key={site.id} className="p-4 hover:shadow-lg transition-shadow">
<div className="space-y-3">
<div className="flex justify-between items-start">
<div className="flex-1">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{site.name}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">
{site.slug}
</p>
</div>
<span
className={`px-2 py-1 text-xs rounded ${
site.is_active
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
: 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200'
}`}
>
{site.is_active ? 'Active' : 'Inactive'}
</span>
</div>
<div className="flex flex-wrap gap-2 items-center">
<SiteTypeBadge hostingType={site.hosting_type} />
<Badge variant="soft" color="neutral" size="xs">
{site.site_type}
</Badge>
</div>
{/* WordPress Integration Button */}
{site.hosting_type === 'wordpress' && (
<div className="pt-2">
<Button
variant={site.has_wordpress_integration ? "outline" : "primary"}
size="sm"
onClick={() => handleIntegration(site.id)}
className="w-full"
startIcon={<PlugIcon className="w-4 h-4" />}
>
{site.has_wordpress_integration ? 'Manage Integration' : 'Connect WordPress'}
</Button>
</div>
)}
<div className="flex items-center justify-between pt-3 border-t border-gray-200 dark:border-gray-700">
<div className="text-xs text-gray-600 dark:text-gray-400">
{site.page_count || 0} pages
</div>
<div className="flex gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleView(site.id)}
title="View"
>
<EyeIcon className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleEdit(site.id)}
title="Edit"
>
<EditIcon className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleSettings(site.id)}
title="Settings"
>
<SettingsIcon className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDelete(site.id)}
title="Delete"
>
<TrashIcon className="w-4 h-4" />
</Button>
</div>
</div>
</div>
</Card>
))}
</div>
)}
</div>
);
}

View File

@@ -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) {