391 lines
15 KiB
TypeScript
391 lines
15 KiB
TypeScript
/**
|
|
* Site Dashboard (Advanced)
|
|
* Phase 7: UI Components & Prompt Management
|
|
* Site overview with statistics and analytics
|
|
*/
|
|
import React, { useState, useEffect } from 'react';
|
|
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, 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,
|
|
GridIcon,
|
|
BoltIcon,
|
|
PageIcon,
|
|
ArrowRightIcon,
|
|
ArrowUpIcon,
|
|
ClockIcon,
|
|
ChevronRightIcon,
|
|
} from '../../icons';
|
|
|
|
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;
|
|
domain?: string;
|
|
industry?: string;
|
|
industry_name?: string;
|
|
}
|
|
|
|
interface SiteSetupState {
|
|
hasIndustry: boolean;
|
|
hasSectors: boolean;
|
|
sectorsCount: number;
|
|
hasWordPressIntegration: boolean;
|
|
hasKeywords: boolean;
|
|
keywordsCount: number;
|
|
hasAuthorProfiles: boolean;
|
|
authorProfilesCount: number;
|
|
}
|
|
|
|
interface OperationStat {
|
|
type: 'clustering' | 'ideas' | 'content' | 'images';
|
|
count: number;
|
|
creditsUsed: number;
|
|
avgCreditsPerOp: number;
|
|
}
|
|
|
|
export default function SiteDashboard() {
|
|
const { id: siteId } = useParams<{ id: string }>();
|
|
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,
|
|
hasSectors: false,
|
|
sectorsCount: 0,
|
|
hasWordPressIntegration: false,
|
|
hasKeywords: false,
|
|
keywordsCount: 0,
|
|
hasAuthorProfiles: false,
|
|
authorProfilesCount: 0,
|
|
});
|
|
const [operations, setOperations] = useState<OperationStat[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
if (siteId) {
|
|
// 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/${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;
|
|
|
|
// Load sectors
|
|
let hasSectors = false;
|
|
let sectorsCount = 0;
|
|
try {
|
|
const sectors = await fetchSiteSectors(Number(currentSiteId));
|
|
hasSectors = sectors && sectors.length > 0;
|
|
sectorsCount = sectors?.length || 0;
|
|
} catch (err) {
|
|
console.log('Could not load sectors');
|
|
}
|
|
|
|
// Check WordPress integration
|
|
let hasWordPressIntegration = false;
|
|
try {
|
|
const wpIntegration = await integrationApi.getWordPressIntegration(Number(currentSiteId));
|
|
hasWordPressIntegration = !!wpIntegration;
|
|
} catch (err) {
|
|
// No integration is fine
|
|
}
|
|
|
|
// Check keywords - try to load keywords for this site
|
|
let hasKeywords = false;
|
|
let keywordsCount = 0;
|
|
try {
|
|
const { fetchKeywords } = await import('../../services/api');
|
|
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) {
|
|
// No keywords is fine
|
|
}
|
|
|
|
// Check author profiles
|
|
let hasAuthorProfiles = false;
|
|
let authorProfilesCount = 0;
|
|
try {
|
|
const authorsData = await fetchAPI(`/v1/thinker/author-profiles/?site_id=${currentSiteId}&page_size=1`);
|
|
hasAuthorProfiles = authorsData?.count > 0;
|
|
authorProfilesCount = authorsData?.count || 0;
|
|
} catch (err) {
|
|
// No profiles is fine
|
|
}
|
|
|
|
setSetupState({
|
|
hasIndustry,
|
|
hasSectors,
|
|
sectorsCount,
|
|
hasWordPressIntegration,
|
|
hasKeywords,
|
|
keywordsCount,
|
|
hasAuthorProfiles,
|
|
authorProfilesCount,
|
|
});
|
|
|
|
// 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}`);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="p-6">
|
|
<PageMeta title="Site Dashboard" />
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="text-gray-500">Loading site dashboard...</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!site) {
|
|
return (
|
|
<div className="p-6">
|
|
<PageMeta title="Site Not Found" />
|
|
<Card className="p-12 text-center">
|
|
<p className="text-gray-600 dark:text-gray-400 mb-4">Site not found</p>
|
|
<Button onClick={() => navigate('/sites')} variant="outline">
|
|
Back to Sites
|
|
</Button>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="p-6">
|
|
<PageMeta title={`${site.name} - Dashboard`} />
|
|
<PageHeader
|
|
title="Site Dashboard"
|
|
badge={{ icon: <GridIcon />, color: 'blue' }}
|
|
hideSiteSector
|
|
/>
|
|
|
|
{/* Site Info Bar */}
|
|
<SiteInfoBar site={site} currentPage="dashboard" />
|
|
|
|
{/* 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}
|
|
hasIndustry={setupState.hasIndustry}
|
|
hasSectors={setupState.hasSectors}
|
|
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
|
|
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} />
|
|
|
|
<CreditAvailabilityWidget
|
|
availableCredits={balance?.credits_remaining ?? 0}
|
|
totalCredits={balance?.plan_credits_per_month ?? 0}
|
|
usedCredits={balance?.credits_used_this_month ?? 0}
|
|
loading={loading}
|
|
/>
|
|
</div>
|
|
|
|
{/* Recent Activity - Placeholder */}
|
|
<Card className="p-6">
|
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
|
Recent Activity
|
|
</h2>
|
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
No recent activity
|
|
</p>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|