Section 2 Part 3

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-03 08:11:41 +00:00
parent 935c7234b1
commit 4d6ee21408
15 changed files with 1209 additions and 895 deletions

View File

@@ -8,16 +8,19 @@ import { useParams, useNavigate } from 'react-router-dom';
import PageMeta from '../../components/common/PageMeta';
import PageHeader from '../../components/common/PageHeader';
import ComponentCard from '../../components/common/ComponentCard';
import SiteInfoBar from '../../components/common/SiteInfoBar';
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchAPI, fetchSiteSectors } from '../../services/api';
import { fetchAPI, fetchSiteSectors, setActiveSite as apiSetActiveSite } from '../../services/api';
import { getDashboardStats } from '../../services/billing.api';
import SiteSetupChecklist from '../../components/sites/SiteSetupChecklist';
import { integrationApi } from '../../services/integration.api';
import SiteConfigWidget from '../../components/dashboard/SiteConfigWidget';
import OperationsCostsWidget from '../../components/dashboard/OperationsCostsWidget';
import CreditAvailabilityWidget from '../../components/dashboard/CreditAvailabilityWidget';
import { useBillingStore } from '../../store/billingStore';
import { useSiteStore } from '../../store/siteStore';
import {
FileIcon,
PlugInIcon,
@@ -27,6 +30,7 @@ import {
ArrowRightIcon,
ArrowUpIcon,
ClockIcon,
ChevronRightIcon,
} from '../../icons';
interface Site {
@@ -67,6 +71,7 @@ export default function SiteDashboard() {
const navigate = useNavigate();
const toast = useToast();
const { balance, loadBalance } = useBillingStore();
const { setActiveSite } = useSiteStore();
const [site, setSite] = useState<Site | null>(null);
const [setupState, setSetupState] = useState<SiteSetupState>({
hasIndustry: false,
@@ -83,19 +88,33 @@ export default function SiteDashboard() {
useEffect(() => {
if (siteId) {
loadSiteData();
loadBalance();
}
}, [siteId, loadBalance]);
const loadSiteData = async () => {
try {
// Create a local copy of siteId to use in async operations
const currentSiteId = siteId;
// Reset state when site changes
setOperations([]);
setSite(null);
setLoading(true);
// Load data for this specific siteId
loadSiteData(currentSiteId);
loadBalance();
}
}, [siteId]);
const loadSiteData = async (currentSiteId: string) => {
try {
// Load site data
const siteData = await fetchAPI(`/v1/auth/sites/${siteId}/`);
const siteData = await fetchAPI(`/v1/auth/sites/${currentSiteId}/`);
// CRITICAL: Verify we're still on the same site before updating state
// This prevents race conditions when user rapidly switches sites
if (siteData) {
setSite(siteData);
// Update global site store so site selector shows correct site
setActiveSite(siteData);
// Also set as active site in backend
await apiSetActiveSite(siteData.id).catch(() => {});
// Check setup state
const hasIndustry = !!siteData.industry || !!siteData.industry_name;
@@ -104,7 +123,7 @@ export default function SiteDashboard() {
let hasSectors = false;
let sectorsCount = 0;
try {
const sectors = await fetchSiteSectors(Number(siteId));
const sectors = await fetchSiteSectors(Number(currentSiteId));
hasSectors = sectors && sectors.length > 0;
sectorsCount = sectors?.length || 0;
} catch (err) {
@@ -114,7 +133,7 @@ export default function SiteDashboard() {
// Check WordPress integration
let hasWordPressIntegration = false;
try {
const wpIntegration = await integrationApi.getWordPressIntegration(Number(siteId));
const wpIntegration = await integrationApi.getWordPressIntegration(Number(currentSiteId));
hasWordPressIntegration = !!wpIntegration;
} catch (err) {
// No integration is fine
@@ -125,7 +144,7 @@ export default function SiteDashboard() {
let keywordsCount = 0;
try {
const { fetchKeywords } = await import('../../services/api');
const keywordsData = await fetchKeywords({ site_id: Number(siteId), page_size: 1 });
const keywordsData = await fetchKeywords({ site_id: Number(currentSiteId), page_size: 1 });
hasKeywords = keywordsData?.results?.length > 0 || keywordsData?.count > 0;
keywordsCount = keywordsData?.count || 0;
} catch (err) {
@@ -136,7 +155,7 @@ export default function SiteDashboard() {
let hasAuthorProfiles = false;
let authorProfilesCount = 0;
try {
const authorsData = await fetchAPI(`/v1/thinker/author-profiles/?site_id=${siteId}&page_size=1`);
const authorsData = await fetchAPI(`/v1/thinker/author-profiles/?site_id=${currentSiteId}&page_size=1`);
hasAuthorProfiles = authorsData?.count > 0;
authorProfilesCount = authorsData?.count || 0;
} catch (err) {
@@ -154,15 +173,54 @@ export default function SiteDashboard() {
authorProfilesCount,
});
// Load operation stats (mock data for now - would come from backend)
// In real implementation, fetch from /api/v1/dashboard/site/{siteId}/operations/
const mockOperations: OperationStat[] = [
{ type: 'clustering', count: 8, creditsUsed: 80, avgCreditsPerOp: 10 },
{ type: 'ideas', count: 12, creditsUsed: 24, avgCreditsPerOp: 2 },
{ type: 'content', count: 28, creditsUsed: 1400, avgCreditsPerOp: 50 },
{ type: 'images', count: 45, creditsUsed: 225, avgCreditsPerOp: 5 },
];
setOperations(mockOperations);
// Load operation stats from real API data
try {
const stats = await getDashboardStats({ site_id: Number(currentSiteId), days: 7 });
// Map operation types from API to display types
const operationTypeMap: Record<string, 'clustering' | 'ideas' | 'content' | 'images'> = {
'clustering': 'clustering',
'idea_generation': 'ideas',
'content_generation': 'content',
'image_generation': 'images',
'image_prompt_extraction': 'images',
};
const mappedOperations: OperationStat[] = [];
const expectedTypes: Array<'clustering' | 'ideas' | 'content' | 'images'> = ['clustering', 'ideas', 'content', 'images'];
// Initialize with zeros
const opTotals: Record<string, { count: number; credits: number }> = {};
expectedTypes.forEach(t => { opTotals[t] = { count: 0, credits: 0 }; });
// Sum up operations by mapped type
if (stats.ai_operations?.operations) {
stats.ai_operations.operations.forEach(op => {
const mappedType = operationTypeMap[op.type] || op.type;
if (opTotals[mappedType]) {
opTotals[mappedType].count += op.count;
opTotals[mappedType].credits += op.credits;
}
});
}
// Convert to array with avgCreditsPerOp
expectedTypes.forEach(type => {
const data = opTotals[type];
mappedOperations.push({
type,
count: data.count,
creditsUsed: data.credits,
avgCreditsPerOp: data.count > 0 ? data.credits / data.count : 0,
});
});
setOperations(mappedOperations);
} catch (err) {
console.log('Could not load operations stats:', err);
// Set empty operations if API fails
setOperations([]);
}
}
} catch (error: any) {
toast.error(`Failed to load site data: ${error.message}`);
@@ -200,33 +258,17 @@ export default function SiteDashboard() {
<div className="p-6">
<PageMeta title={`${site.name} - Dashboard`} />
<PageHeader
title={site.name}
title="Site Dashboard"
badge={{ icon: <GridIcon />, color: 'blue' }}
breadcrumb="Sites / Dashboard"
hideSiteSector
/>
{/* Site Info */}
<div className="mb-6">
<p className="text-sm text-gray-600 dark:text-gray-400">
{site.slug} {site.site_type} {site.hosting_type}
</p>
{site.domain && (
<p className="text-sm text-gray-500 dark:text-gray-500 mt-1">
{site.domain}
</p>
)}
<div className="flex gap-2 mt-4">
<Button
variant="primary"
onClick={() => navigate(`/sites/${siteId}/settings`)}
>
Settings
</Button>
</div>
</div>
{/* Site Info Bar */}
<SiteInfoBar site={site} currentPage="dashboard" />
{/* Site Setup Checklist */}
<div className="mb-6">
{/* Site Setup Progress + Quick Actions - Side by Side */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
{/* Site Setup Checklist - Left Half */}
<SiteSetupChecklist
siteId={Number(siteId)}
siteName={site.name}
@@ -235,22 +277,95 @@ export default function SiteDashboard() {
hasWordPressIntegration={setupState.hasWordPressIntegration}
hasKeywords={setupState.hasKeywords}
/>
{/* Quick Actions - Right Half */}
<ComponentCard title="Quick Actions" desc="Common site management tasks">
<div className="grid grid-cols-2 gap-3">
{/* Manage Pages */}
<button
onClick={() => navigate(`/sites/${siteId}/pages`)}
className="flex items-center gap-3 px-4 py-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 hover:border-brand-300 hover:bg-brand-50 dark:hover:bg-brand-900/10 transition-all group"
>
<div className="size-8 rounded-lg bg-brand-100 dark:bg-brand-900/30 flex items-center justify-center flex-shrink-0">
<PageIcon className="h-4 w-4 text-brand-600 dark:text-brand-400" />
</div>
<span className="flex-1 text-sm font-medium text-gray-700 dark:text-gray-300 text-left">Manage Pages</span>
</button>
{/* Manage Content */}
<button
onClick={() => navigate(`/sites/${siteId}/content`)}
className="flex items-center gap-3 px-4 py-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 hover:border-success-300 hover:bg-success-50 dark:hover:bg-success-900/10 transition-all group"
>
<div className="size-8 rounded-lg bg-success-100 dark:bg-success-900/30 flex items-center justify-center flex-shrink-0">
<FileIcon className="h-4 w-4 text-success-600 dark:text-success-400" />
</div>
<span className="flex-1 text-sm font-medium text-gray-700 dark:text-gray-300 text-left">Manage Content</span>
</button>
{/* Integrations */}
<button
onClick={() => navigate(`/sites/${siteId}/settings?tab=integrations`)}
className="flex items-center gap-3 px-4 py-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 hover:border-purple-300 hover:bg-purple-50 dark:hover:bg-purple-900/10 transition-all group"
>
<div className="size-8 rounded-lg bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center flex-shrink-0">
<PlugInIcon className="h-4 w-4 text-purple-600 dark:text-purple-400" />
</div>
<span className="flex-1 text-sm font-medium text-gray-700 dark:text-gray-300 text-left">Integrations</span>
</button>
{/* Sync Dashboard */}
<button
onClick={() => navigate(`/sites/${siteId}/sync`)}
className="flex items-center gap-3 px-4 py-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 hover:border-warning-300 hover:bg-warning-50 dark:hover:bg-warning-900/10 transition-all group"
>
<div className="size-8 rounded-lg bg-warning-100 dark:bg-warning-900/30 flex items-center justify-center flex-shrink-0">
<BoltIcon className="h-4 w-4 text-warning-600 dark:text-warning-400" />
</div>
<span className="flex-1 text-sm font-medium text-gray-700 dark:text-gray-300 text-left">Sync Dashboard</span>
</button>
{/* Deploy Site */}
<button
onClick={() => navigate(`/sites/${siteId}/deploy`)}
className="flex items-center gap-3 px-4 py-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 hover:border-info-300 hover:bg-info-50 dark:hover:bg-info-900/10 transition-all group"
>
<div className="size-8 rounded-lg bg-info-100 dark:bg-info-900/30 flex items-center justify-center flex-shrink-0">
<ArrowUpIcon className="h-4 w-4 text-info-600 dark:text-info-400" />
</div>
<span className="flex-1 text-sm font-medium text-gray-700 dark:text-gray-300 text-left">Deploy Site</span>
</button>
{/* Content Calendar */}
<button
onClick={() => navigate(`/publisher/content-calendar`)}
className="flex items-center gap-3 px-4 py-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 hover:border-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-all group"
>
<div className="size-8 rounded-lg bg-gray-100 dark:bg-gray-800 flex items-center justify-center flex-shrink-0">
<ClockIcon className="h-4 w-4 text-gray-600 dark:text-gray-400" />
</div>
<span className="flex-1 text-sm font-medium text-gray-700 dark:text-gray-300 text-left">Content Calendar</span>
</button>
</div>
</ComponentCard>
</div>
{/* Site Insights - 3 Column Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
<SiteConfigWidget
setupState={{
hasIndustry: setupState.hasIndustry,
sectorsCount: setupState.sectorsCount,
hasWordPressIntegration: setupState.hasWordPressIntegration,
keywordsCount: setupState.keywordsCount,
authorProfilesCount: setupState.authorProfilesCount
}}
siteId={Number(siteId)}
siteName={site.name}
hasIndustry={setupState.hasIndustry}
hasSectors={setupState.hasSectors}
sectorsCount={setupState.sectorsCount}
hasWordPress={setupState.hasWordPressIntegration}
hasKeywords={setupState.hasKeywords}
keywordsCount={setupState.keywordsCount}
hasAuthorProfiles={setupState.hasAuthorProfiles}
authorProfilesCount={setupState.authorProfilesCount}
/>
<OperationsCostsWidget operations={operations} siteId={Number(siteId)} />
<OperationsCostsWidget operations={operations} />
<CreditAvailabilityWidget
availableCredits={balance?.credits_remaining ?? 0}
@@ -260,109 +375,8 @@ export default function SiteDashboard() {
/>
</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">
<Button
onClick={() => navigate(`/sites/${siteId}/pages`)}
variant="ghost"
tone="neutral"
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-primary)] hover:shadow-lg transition-all group h-auto justify-start"
>
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] flex items-center justify-center text-white shadow-lg">
<PageIcon className="h-6 w-6" />
</div>
<div className="flex-1 text-left">
<h4 className="font-semibold text-gray-900 mb-1">Manage Pages</h4>
<p className="text-sm text-gray-600">View and edit pages</p>
</div>
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-primary)] transition" />
</Button>
<Button
onClick={() => navigate(`/sites/${siteId}/content`)}
variant="ghost"
tone="neutral"
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-success)] hover:shadow-lg transition-all group h-auto justify-start"
>
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-success)] to-[var(--color-success-dark)] flex items-center justify-center text-white shadow-lg">
<FileIcon className="h-6 w-6" />
</div>
<div className="flex-1 text-left">
<h4 className="font-semibold text-gray-900 mb-1">Manage Content</h4>
<p className="text-sm text-gray-600">View and edit content</p>
</div>
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-success)] transition" />
</Button>
<Button
onClick={() => navigate(`/sites/${siteId}/settings?tab=integrations`)}
variant="ghost"
tone="neutral"
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-purple)] hover:shadow-lg transition-all group h-auto justify-start"
>
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-purple)] to-[var(--color-purple-dark)] flex items-center justify-center text-white shadow-lg">
<PlugInIcon className="h-6 w-6" />
</div>
<div className="flex-1 text-left">
<h4 className="font-semibold text-gray-900 mb-1">Integrations</h4>
<p className="text-sm text-gray-600">Manage connections</p>
</div>
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-purple)] transition" />
</Button>
<Button
onClick={() => navigate(`/sites/${siteId}/sync`)}
variant="ghost"
tone="neutral"
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-warning)] hover:shadow-lg transition-all group h-auto justify-start"
>
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-warning)] to-[var(--color-warning-dark)] flex items-center justify-center text-white shadow-lg">
<BoltIcon className="h-6 w-6" />
</div>
<div className="flex-1 text-left">
<h4 className="font-semibold text-gray-900 mb-1">Sync Dashboard</h4>
<p className="text-sm text-gray-600">View sync status</p>
</div>
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-warning)] transition" />
</Button>
<Button
onClick={() => navigate(`/sites/${siteId}/deploy`)}
variant="ghost"
tone="neutral"
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-primary)] hover:shadow-lg transition-all group h-auto justify-start"
>
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] flex items-center justify-center text-white shadow-lg">
<ArrowUpIcon className="h-6 w-6" />
</div>
<div className="flex-1 text-left">
<h4 className="font-semibold text-gray-900 mb-1">Deploy Site</h4>
<p className="text-sm text-gray-600">Deploy to production</p>
</div>
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-primary)] transition" />
</Button>
<Button
onClick={() => navigate(`/publisher/content-calendar`)}
variant="ghost"
tone="neutral"
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-amber-500 hover:shadow-lg transition-all group h-auto justify-start"
>
<div className="size-12 rounded-xl bg-gradient-to-br from-amber-500 to-amber-600 flex items-center justify-center text-white shadow-lg">
<ClockIcon className="h-6 w-6" />
</div>
<div className="flex-1 text-left">
<h4 className="font-semibold text-gray-900 mb-1">Content Calendar</h4>
<p className="text-sm text-gray-600">Schedule and manage content publishing</p>
</div>
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-warning-500 transition" />
</Button>
</div>
</ComponentCard>
{/* Recent Activity - Placeholder */}
<Card className="p-6 mt-6">
<Card className="p-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Recent Activity
</h2>