Pre luanch plan phase 1 complete

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-05 03:40:39 +00:00
parent 1f2e734ea2
commit e93ea77c2b
60 changed files with 492 additions and 5215 deletions

View File

@@ -25,18 +25,15 @@ const Privacy = lazy(() => import("./pages/legal/Privacy"));
const Home = lazy(() => import("./pages/Dashboard/Home"));
// Planner Module - Lazy loaded
const PlannerDashboard = lazy(() => import("./pages/Planner/Dashboard"));
const Keywords = lazy(() => import("./pages/Planner/Keywords"));
const Clusters = lazy(() => import("./pages/Planner/Clusters"));
const ClusterDetail = lazy(() => import("./pages/Planner/ClusterDetail"));
const Ideas = lazy(() => import("./pages/Planner/Ideas"));
// Writer Module - Lazy loaded
const WriterDashboard = lazy(() => import("./pages/Writer/Dashboard"));
const Tasks = lazy(() => import("./pages/Writer/Tasks"));
const Content = lazy(() => import("./pages/Writer/Content"));
const ContentView = lazy(() => import("./pages/Writer/ContentView"));
const Drafts = lazy(() => import("./pages/Writer/Drafts"));
const Images = lazy(() => import("./pages/Writer/Images"));
const Review = lazy(() => import("./pages/Writer/Review"));
const Approved = lazy(() => import("./pages/Writer/Approved"));
@@ -45,16 +42,13 @@ const Approved = lazy(() => import("./pages/Writer/Approved"));
const AutomationPage = lazy(() => import("./pages/Automation/AutomationPage"));
// Linker Module - Lazy loaded
const LinkerDashboard = lazy(() => import("./pages/Linker/Dashboard"));
const LinkerContentList = lazy(() => import("./pages/Linker/ContentList"));
// Optimizer Module - Lazy loaded
const OptimizerDashboard = lazy(() => import("./pages/Optimizer/Dashboard"));
const OptimizerContentSelector = lazy(() => import("./pages/Optimizer/ContentSelector"));
const AnalysisPreview = lazy(() => import("./pages/Optimizer/AnalysisPreview"));
// Thinker Module - Lazy loaded
const ThinkerDashboard = lazy(() => import("./pages/Thinker/Dashboard"));
// Thinker Module - Lazy loaded (Admin Only)
const Prompts = lazy(() => import("./pages/Thinker/Prompts"));
const AuthorProfiles = lazy(() => import("./pages/Thinker/AuthorProfiles"));
const ThinkerProfile = lazy(() => import("./pages/Thinker/Profile"));
@@ -104,7 +98,6 @@ const PostEditor = lazy(() => import("./pages/Sites/PostEditor"));
const SiteSettings = lazy(() => import("./pages/Sites/Settings"));
const SyncDashboard = lazy(() => import("./pages/Sites/SyncDashboard"));
const DeploymentPanel = lazy(() => import("./pages/Sites/DeploymentPanel"));
const PublishingQueue = lazy(() => import("./pages/Sites/PublishingQueue"));
// Publisher Module - Lazy loaded
const ContentCalendar = lazy(() => import("./pages/Publisher/ContentCalendar"));

View File

@@ -376,9 +376,8 @@ export default function SignUpFormEnhanced({ planDetails: planDetailsProp, planL
</div>
{isPaidPlan ? (
<Button type="button" variant="primary" onClick={handleNextStep} className="w-full">
<Button type="button" variant="primary" onClick={handleNextStep} className="w-full" endIcon={<ChevronRightIcon className="w-4 h-4" />}>
Continue to Billing
<ChevronRightIcon className="w-4 h-4 ml-2" />
</Button>
) : (
<Button type="submit" variant="primary" disabled={loading} className="w-full">
@@ -401,9 +400,8 @@ export default function SignUpFormEnhanced({ planDetails: planDetailsProp, planL
<Button type="button" variant="outline" onClick={handlePrevStep} className="flex-1">
Back
</Button>
<Button type="button" variant="primary" onClick={handleNextStep} className="flex-1">
<Button type="button" variant="primary" onClick={handleNextStep} className="flex-1" endIcon={<ChevronRightIcon className="w-4 h-4" />}>
Continue to Payment
<ChevronRightIcon className="w-4 h-4 ml-2" />
</Button>
</div>
</div>

View File

@@ -0,0 +1,31 @@
/**
* Page Loader Component - Global page loading indicator
* Displays a consistent spinner overlay when pages are loading
* Used in AppLayout to provide unified loading experience across all pages
*/
import React from 'react';
import { Spinner } from '../ui/spinner/Spinner';
import { usePageLoadingContext } from '../../context/PageLoadingContext';
interface PageLoaderProps {
className?: string;
}
export const PageLoader: React.FC<PageLoaderProps> = ({ className = '' }) => {
const { isLoading, loadingMessage } = usePageLoadingContext();
if (!isLoading) return null;
return (
<div className={`flex flex-col items-center justify-center py-20 ${className}`}>
<Spinner size="lg" color="primary" />
{loadingMessage && (
<p className="mt-4 text-sm text-gray-500 dark:text-gray-400">
{loadingMessage}
</p>
)}
</div>
);
};
export default PageLoader;

View File

@@ -52,8 +52,8 @@ export default function SiteCard({
return (
<article className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/3">
<div className="relative p-5 pb-9">
<div className="flex items-center gap-3 mb-3">
<div className="relative p-3 pb-5">
<div className="flex items-center gap-2 mb-1">
<div className="inline-flex items-center justify-center">
{icon}
</div>
@@ -61,32 +61,34 @@ export default function SiteCard({
{site.name}
</h3>
</div>
<p className="max-w-xs text-sm text-gray-500 dark:text-gray-400 mb-2">
{site.description || 'No description'}
</p>
{site.domain && (
<p className="text-xs text-gray-400 dark:text-gray-500 mb-2">
<p className="text-xs text-gray-400 dark:text-gray-500 mb-2 ml-8">
{site.domain}
</p>
)}
<div className="flex items-center gap-2 mb-2 flex-wrap">
<SiteTypeBadge hostingType={site.hosting_type} />
{site.description && (
<p className="max-w-xs text-sm text-gray-500 dark:text-gray-400 mb-2">
{site.description}
</p>
)}
<div className="flex items-center gap-1.5 mb-2 flex-wrap">
<SiteTypeBadge hostingType={site.hosting_type} size="xs" />
{site.industry_name && (
<Badge variant="soft" color="warning" size="sm">
<Badge variant="soft" color="warning" size="xs">
{site.industry_name}
</Badge>
)}
<Badge variant="soft" color="neutral" size="sm">
<Badge variant="soft" color="neutral" size="xs">
{site.active_sectors_count} / 5 Sectors
</Badge>
{site.status && (
<Badge variant={site.is_active ? 'solid' : 'soft'} color={site.status === 'active' ? 'success' : 'neutral'} size="sm">
<Badge variant={site.is_active ? 'solid' : 'soft'} color={site.status === 'active' ? 'success' : 'neutral'} size="xs">
{site.status}
</Badge>
)}
</div>
{/* Setup Checklist - Compact View */}
<div className="mt-3">
<div className="mt-2">
<SiteSetupChecklist
siteId={site.id}
siteName={site.name}
@@ -98,18 +100,18 @@ export default function SiteCard({
/>
</div>
{/* Status Text and Circle - Same row */}
<div className="absolute top-5 right-5 flex items-center gap-2">
<div className="absolute top-3 right-3 flex items-center gap-2">
<span className={`text-sm ${statusText.color} ${statusText.bold ? 'font-bold' : ''} transition-colors duration-200`}>
{statusText.text}
</span>
<div
className={`w-[25px] h-[25px] rounded-full ${getStatusColor()} transition-colors duration-200`}
className={`w-[20px] h-[20px] rounded-full ${getStatusColor()} transition-colors duration-200`}
title={site.is_active ? 'Active site' : 'Inactive site'}
/>
</div>
</div>
<div className="flex items-center justify-between border-t border-gray-200 p-4 dark:border-gray-800">
<div className="flex gap-2 flex-1">
<div className="flex items-center justify-between border-t border-gray-200 p-2 dark:border-gray-800">
<div className="flex gap-1.5 flex-1">
<Button
variant="primary"
tone="brand"

View File

@@ -9,7 +9,7 @@ import Badge from '../ui/badge/Badge';
interface SiteTypeBadgeProps {
hostingType: string;
className?: string;
size?: 'sm' | 'md' | 'lg';
size?: 'xs' | 'sm' | 'md' | 'lg';
}
export default function SiteTypeBadge({ hostingType, className = '', size = 'sm' }: SiteTypeBadgeProps) {

View File

@@ -0,0 +1,74 @@
/**
* Page Loading Context - Global page loading state management
* Provides a centralized loading indicator for page transitions and data fetching
* Pages can use usePageLoading hook to show/hide the global spinner
*/
import React, { createContext, useContext, useState, useCallback, ReactNode, useRef } from 'react';
interface PageLoadingContextType {
isLoading: boolean;
loadingMessage: string | null;
setLoading: (loading: boolean, message?: string) => void;
startLoading: (message?: string) => void;
stopLoading: () => void;
}
const PageLoadingContext = createContext<PageLoadingContextType | undefined>(undefined);
export function PageLoadingProvider({ children }: { children: ReactNode }) {
const [isLoading, setIsLoading] = useState(false);
const [loadingMessage, setLoadingMessage] = useState<string | null>(null);
const loadingCountRef = useRef(0);
const setLoading = useCallback((loading: boolean, message?: string) => {
if (loading) {
loadingCountRef.current++;
setIsLoading(true);
if (message) setLoadingMessage(message);
} else {
loadingCountRef.current = Math.max(0, loadingCountRef.current - 1);
if (loadingCountRef.current === 0) {
setIsLoading(false);
setLoadingMessage(null);
}
}
}, []);
const startLoading = useCallback((message?: string) => {
setLoading(true, message);
}, [setLoading]);
const stopLoading = useCallback(() => {
setLoading(false);
}, [setLoading]);
return (
<PageLoadingContext.Provider value={{ isLoading, loadingMessage, setLoading, startLoading, stopLoading }}>
{children}
</PageLoadingContext.Provider>
);
}
export function usePageLoadingContext() {
const context = useContext(PageLoadingContext);
if (context === undefined) {
throw new Error('usePageLoadingContext must be used within a PageLoadingProvider');
}
return context;
}
/**
* Hook for pages to manage loading state
* Automatically tracks loading state and provides helper functions
*/
export function usePageLoading() {
const { isLoading, loadingMessage, startLoading, stopLoading, setLoading } = usePageLoadingContext();
return {
isLoading,
loadingMessage,
startLoading,
stopLoading,
setLoading,
};
}

View File

@@ -11,6 +11,8 @@ import { useHeaderMetrics } from "../context/HeaderMetricsContext";
import { useErrorHandler } from "../hooks/useErrorHandler";
import { trackLoading } from "../components/common/LoadingStateMonitor";
import PendingPaymentBanner from "../components/billing/PendingPaymentBanner";
import { PageLoadingProvider, usePageLoadingContext } from "../context/PageLoadingContext";
import PageLoader from "../components/common/PageLoader";
const LayoutContent: React.FC = () => {
const { isExpanded, isHovered, isMobileOpen } = useSidebar();
@@ -139,17 +141,9 @@ const LayoutContent: React.FC = () => {
accentColor = 'purple';
}
// Format credit value with K/M suffix for large numbers
// Format credit value with full number and comma separators
const formatCredits = (val: number): string => {
if (val >= 1000000) {
const millions = val / 1000000;
return millions % 1 === 0 ? `${millions}M` : `${millions.toFixed(1)}M`;
}
if (val >= 1000) {
const thousands = val / 1000;
return thousands % 1 === 0 ? `${thousands}K` : `${thousands.toFixed(1)}K`;
}
return val.toString();
return val.toLocaleString();
};
// Set credit balance - show as "used/total Credits"
@@ -174,18 +168,33 @@ const LayoutContent: React.FC = () => {
<AppHeader />
{/* Pending Payment Banner - Shows when account status is 'pending_payment' */}
<PendingPaymentBanner className="mx-4 mt-2 md:mx-6 md:mt-2" />
<div className="px-4 pt-1.5 pb-20 md:px-6 md:pt-1.5 md:pb-24">
<div className="p-3">
<PageLoaderWrapper>
<Outlet />
</PageLoaderWrapper>
</div>
</div>
</div>
);
};
// Wrapper component to conditionally render Outlet or PageLoader
const PageLoaderWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { isLoading } = usePageLoadingContext();
if (isLoading) {
return <PageLoader />;
}
return <>{children}</>;
};
const AppLayout: React.FC = () => {
return (
<SidebarProvider>
<PageLoadingProvider>
<LayoutContent />
</PageLoadingProvider>
</SidebarProvider>
);
};

View File

@@ -1,22 +0,0 @@
import PageMeta from "../components/common/PageMeta";
import ComponentCard from "../components/common/ComponentCard";
export default function Analytics() {
return (
<>
<PageMeta title="Analytics - IGNY8" description="Performance analytics" />
<ComponentCard title="Coming Soon" desc="Performance analytics">
<div className="text-center py-8">
<p className="text-gray-600 dark:text-gray-400">
Analytics - Coming Soon
</p>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">
Performance analytics and reporting for data-driven decisions
</p>
</div>
</ComponentCard>
</>
);
}

View File

@@ -1,389 +0,0 @@
/**
* Automation Dashboard Page
* Main page for managing AI automation pipeline
*/
import React, { useState, useEffect } from 'react';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { useSiteStore } from '../../store/siteStore';
import { automationService, AutomationRun, AutomationConfig, PipelineStage } from '../../services/automationService';
import StageCard from '../../components/Automation/StageCard';
import ActivityLog from '../../components/Automation/ActivityLog';
import ConfigModal from '../../components/Automation/ConfigModal';
import RunHistory from '../../components/Automation/RunHistory';
import PageMeta from '../../components/common/PageMeta';
import ComponentCard from '../../components/common/ComponentCard';
import DebugSiteSelector from '../../components/common/DebugSiteSelector';
import Button from '../../components/ui/button/Button';
import { BoltIcon } from '../../icons';
const STAGE_NAMES = [
'Keywords → Clusters',
'Clusters → Ideas',
'Ideas → Tasks',
'Tasks → Content',
'Content → Image Prompts',
'Image Prompts → Images',
'Manual Review Gate',
];
const AutomationPage: React.FC = () => {
const { activeSite } = useSiteStore();
const toast = useToast();
const [config, setConfig] = useState<AutomationConfig | null>(null);
const [currentRun, setCurrentRun] = useState<AutomationRun | null>(null);
const [pipelineOverview, setPipelineOverview] = useState<PipelineStage[]>([]);
const [showConfigModal, setShowConfigModal] = useState(false);
const [loading, setLoading] = useState(true);
const [lastUpdated, setLastUpdated] = useState<Date>(new Date());
const [estimate, setEstimate] = useState<{ estimated_credits: number; current_balance: number; sufficient: boolean } | null>(null);
// Poll for current run updates
useEffect(() => {
if (!activeSite) return;
loadData();
// Poll every 5 seconds when run is active
const interval = setInterval(() => {
if (currentRun && (currentRun.status === 'running' || currentRun.status === 'paused')) {
loadCurrentRun();
} else {
// Refresh pipeline overview when not running
loadPipelineOverview();
}
}, 5000);
return () => clearInterval(interval);
}, [activeSite, currentRun?.status]);
const loadData = async () => {
if (!activeSite) return;
try {
setLoading(true);
const [configData, runData, estimateData, pipelineData] = await Promise.all([
automationService.getConfig(activeSite.id),
automationService.getCurrentRun(activeSite.id),
automationService.estimate(activeSite.id),
automationService.getPipelineOverview(activeSite.id),
]);
setConfig(configData);
setCurrentRun(runData.run);
setEstimate(estimateData);
setPipelineOverview(pipelineData.stages);
setLastUpdated(new Date());
} catch (error: any) {
toast.error('Failed to load automation data');
console.error(error);
} finally {
setLoading(false);
}
};
const loadCurrentRun = async () => {
if (!activeSite) return;
try {
const data = await automationService.getCurrentRun(activeSite.id);
setCurrentRun(data.run);
} catch (error) {
console.error('Failed to poll current run', error);
}
};
const loadPipelineOverview = async () => {
if (!activeSite) return;
try {
const data = await automationService.getPipelineOverview(activeSite.id);
setPipelineOverview(data.stages);
} catch (error) {
console.error('Failed to poll pipeline overview', error);
}
};
const handleRunNow = async () => {
if (!activeSite) return;
// Check credit balance
if (estimate && !estimate.sufficient) {
toast.error(`Insufficient credits. Need ~${estimate.estimated_credits}, you have ${estimate.current_balance}`);
return;
}
try {
const result = await automationService.runNow(activeSite.id);
toast.success('Automation started');
loadCurrentRun();
} catch (error: any) {
toast.error(error.response?.data?.error || 'Failed to start automation');
}
};
const handlePause = async () => {
if (!currentRun) return;
try {
await automationService.pause(currentRun.run_id);
toast.success('Automation paused');
loadCurrentRun();
} catch (error) {
toast.error('Failed to pause automation');
}
};
const handleResume = async () => {
if (!currentRun) return;
try {
await automationService.resume(currentRun.run_id);
toast.success('Automation resumed');
loadCurrentRun();
} catch (error) {
toast.error('Failed to resume automation');
}
};
const handleSaveConfig = async (newConfig: Partial<AutomationConfig>) => {
if (!activeSite) return;
try {
await automationService.updateConfig(activeSite.id, newConfig);
toast.success('Configuration saved');
setShowConfigModal(false);
loadData();
} catch (error) {
toast.error('Failed to save configuration');
}
};
if (loading) {
return (
<div className="flex items-center justify-center min-h-[60vh]">
<div className="text-lg text-gray-600 dark:text-gray-400">Loading automation...</div>
</div>
);
}
if (!activeSite) {
return (
<div className="flex items-center justify-center min-h-[60vh]">
<div className="text-lg text-gray-600 dark:text-gray-400">Please select a site to view automation</div>
</div>
);
}
return (
<>
<PageMeta
title="AI Automation Pipeline | IGNY8"
description="Automated content creation from keywords to published articles"
/>
<div className="space-y-6">
{/* Page Header with Site Selector (no sector) */}
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div className="flex-1">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-10 h-10 rounded-xl bg-purple-600 dark:bg-purple-500 flex-shrink-0">
<BoltIcon className="text-white size-5" />
</div>
<h2 className="text-2xl font-bold text-gray-800 dark:text-white/90">AI Automation Pipeline</h2>
</div>
{activeSite && (
<div className="flex items-center gap-3 mt-1">
{lastUpdated && (
<p className="text-sm text-gray-500 dark:text-gray-400">
Last updated: {lastUpdated.toLocaleTimeString()}
</p>
)}
<span className="text-sm text-gray-400 dark:text-gray-600"></span>
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">
Site: <span className="text-brand-600 dark:text-brand-400">{activeSite.name}</span>
</p>
</div>
)}
</div>
<div className="flex items-center gap-3">
<DebugSiteSelector />
</div>
</div>
{/* Schedule Status Card */}
{config && (
<ComponentCard
title="Schedule & Status"
desc="Configure and monitor your automation schedule"
>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div>
<div className="text-sm text-gray-600 dark:text-gray-400">Status</div>
<div className="font-semibold mt-1">
{config.is_enabled ? (
<span className="text-success-600 dark:text-success-400"> Enabled</span>
) : (
<span className="text-gray-600 dark:text-gray-400"> Disabled</span>
)}
</div>
</div>
<div>
<div className="text-sm text-gray-600 dark:text-gray-400">Schedule</div>
<div className="font-semibold mt-1 capitalize">
{config.frequency} at {config.scheduled_time}
</div>
</div>
<div>
<div className="text-sm text-gray-600 dark:text-gray-400">Last Run</div>
<div className="font-semibold mt-1">
{config.last_run_at
? new Date(config.last_run_at).toLocaleString()
: 'Never'}
</div>
</div>
<div>
<div className="text-sm text-gray-600 dark:text-gray-400">Estimated Credits</div>
<div className="font-semibold mt-1">
{estimate?.estimated_credits || 0} credits
{estimate && !estimate.sufficient && (
<span className="text-error-600 dark:text-error-400 ml-2">(Insufficient)</span>
)}
</div>
</div>
</div>
<div className="flex gap-3 mt-6">
<Button
onClick={() => setShowConfigModal(true)}
variant="outline"
tone="brand"
>
Configure
</Button>
{currentRun?.status === 'running' && (
<Button
onClick={handlePause}
variant="primary"
tone="warning"
>
Pause
</Button>
)}
{currentRun?.status === 'paused' && (
<Button
onClick={handleResume}
variant="primary"
tone="brand"
>
Resume
</Button>
)}
{!currentRun && (
<Button
onClick={handleRunNow}
variant="primary"
tone="success"
disabled={!config?.is_enabled}
>
Run Now
</Button>
)}
</div>
</ComponentCard>
)}
{/* Pipeline Overview - Always Visible */}
<ComponentCard
title="📊 Pipeline Overview"
desc="Complete view of automation pipeline status and pending items"
>
<div className="mb-4 flex items-center justify-between">
<div className="text-sm text-gray-600 dark:text-gray-400">
{currentRun ? (
<>
<span className="font-semibold text-brand-600 dark:text-brand-400"> Live Run Active</span> - Stage {currentRun.current_stage} of 7
</>
) : (
<>
<span className="font-semibold text-gray-700 dark:text-gray-300">Pipeline Status</span> - Ready to run
</>
)}
</div>
<div className="text-sm">
{pipelineOverview.reduce((sum, stage) => sum + stage.pending, 0)} total items pending
</div>
</div>
{/* Stage Cards Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-7 gap-3">
{STAGE_NAMES.map((name, index) => (
<StageCard
key={index}
stageNumber={index + 1}
stageName={name}
currentStage={currentRun?.current_stage || 0}
result={currentRun ? (currentRun[`stage_${index + 1}_result` as keyof AutomationRun] as any) : null}
pipelineData={pipelineOverview[index]}
/>
))}
</div>
</ComponentCard>
{/* Current Run Status */}
{currentRun && (
<ComponentCard
title={`🔄 Current Run: ${currentRun.run_id}`}
desc="Live automation progress and detailed results"
>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div>
<div className="text-sm text-gray-600 dark:text-gray-400">Status</div>
<div className="font-semibold mt-1 capitalize">
{currentRun.status === 'running' && <span className="text-brand-600 dark:text-brand-400"> {currentRun.status}</span>}
{currentRun.status === 'paused' && <span className="text-warning-600 dark:text-warning-400"> {currentRun.status}</span>}
{currentRun.status === 'completed' && <span className="text-success-600 dark:text-success-400"> {currentRun.status}</span>}
{currentRun.status === 'failed' && <span className="text-error-600 dark:text-error-400"> {currentRun.status}</span>}
</div>
</div>
<div>
<div className="text-sm text-gray-600 dark:text-gray-400">Current Stage</div>
<div className="font-semibold mt-1">
Stage {currentRun.current_stage}: {STAGE_NAMES[currentRun.current_stage - 1]}
</div>
</div>
<div>
<div className="text-sm text-gray-600 dark:text-gray-400">Started</div>
<div className="font-semibold mt-1">
{new Date(currentRun.started_at).toLocaleString()}
</div>
</div>
<div>
<div className="text-sm text-gray-600 dark:text-gray-400">Credits Used</div>
<div className="font-semibold mt-1 text-purple-600 dark:text-purple-400">{currentRun.total_credits_used}</div>
</div>
</div>
</ComponentCard>
)}
{/* Activity Log */}
{currentRun && (
<ActivityLog runId={currentRun.run_id} />
)}
{/* Run History */}
<RunHistory siteId={activeSite.id} />
{/* Config Modal */}
{showConfigModal && config && (
<ConfigModal
config={config}
onSave={handleSaveConfig}
onCancel={() => setShowConfigModal(false)}
/>
)}
</div>
</>
);
};
export default AutomationPage;

View File

@@ -7,11 +7,12 @@ import { Card } from '../../components/ui/card';
import Badge from '../../components/ui/badge/Badge';
import Button from '../../components/ui/button/Button';
import { ArrowUpIcon } from '../../icons';
import { usePageLoading } from '../../context/PageLoadingContext';
export default function Credits() {
const toast = useToast();
const { startLoading, stopLoading } = usePageLoading();
const [balance, setBalance] = useState<CreditBalance | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadBalance();
@@ -19,29 +20,18 @@ export default function Credits() {
const loadBalance = async () => {
try {
setLoading(true);
startLoading('Loading content usage...');
const data = await getCreditBalance();
setBalance(data);
} catch (error: any) {
toast.error(`Failed to load content usage: ${error.message}`);
} finally {
setLoading(false);
stopLoading();
}
};
if (loading) {
return (
<div className="p-6">
<PageMeta title="Content Usage" />
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading...</div>
</div>
</div>
);
}
return (
<div className="p-6">
<>
<PageMeta title="Content Usage" />
<div className="flex items-center justify-between mb-6">
<div>
@@ -126,7 +116,7 @@ export default function Credits() {
</div>
</Card>
</div>
</div>
</>
);
}

View File

@@ -4,11 +4,12 @@ import { useToast } from '../../components/ui/toast/ToastContainer';
import { getCreditTransactions, CreditTransaction } from '../../services/billing.api';
import { Card } from '../../components/ui/card';
import Badge from '../../components/ui/badge/Badge';
import { usePageLoading } from '../../context/PageLoadingContext';
export default function Transactions() {
const toast = useToast();
const { startLoading, stopLoading } = usePageLoading();
const [transactions, setTransactions] = useState<CreditTransaction[]>([]);
const [loading, setLoading] = useState(true);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
@@ -18,7 +19,7 @@ export default function Transactions() {
const loadTransactions = async () => {
try {
setLoading(true);
startLoading('Loading transactions...');
const response = await getCreditTransactions();
setTransactions(response.results || []);
const count = response.count || 0;
@@ -26,7 +27,7 @@ export default function Transactions() {
} catch (error: any) {
toast.error(`Failed to load transactions: ${error.message}`);
} finally {
setLoading(false);
stopLoading();
}
};
@@ -47,18 +48,13 @@ export default function Transactions() {
};
return (
<div className="p-6">
<>
<PageMeta title="Credit Transactions" />
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Credit Transactions</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">View all credit transactions and history</p>
</div>
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading...</div>
</div>
) : (
<Card className="p-6">
<div className="overflow-x-auto">
<table className="w-full">
@@ -101,8 +97,7 @@ export default function Transactions() {
</table>
</div>
</Card>
)}
</div>
</>
);
}

View File

@@ -4,6 +4,7 @@ import { useToast } from '../../components/ui/toast/ToastContainer';
import { getCreditTransactions, getCreditBalance, CreditTransaction as BillingTransaction, CreditBalance } from '../../services/billing.api';
import { Card } from '../../components/ui/card';
import Badge from '../../components/ui/badge/Badge';
import { usePageLoading } from '../../context/PageLoadingContext';
// Credit costs per operation (Phase 0: Credit-only system)
const CREDIT_COSTS: Record<string, { cost: number | string; description: string }> = {
@@ -20,9 +21,9 @@ const CREDIT_COSTS: Record<string, { cost: number | string; description: string
export default function Usage() {
const toast = useToast();
const { startLoading, stopLoading } = usePageLoading();
const [transactions, setTransactions] = useState<BillingTransaction[]>([]);
const [balance, setBalance] = useState<CreditBalance | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadUsage();
@@ -30,7 +31,7 @@ export default function Usage() {
const loadUsage = async () => {
try {
setLoading(true);
startLoading('Loading usage data...');
const [txnData, balanceData] = await Promise.all([
getCreditTransactions(),
getCreditBalance()
@@ -40,23 +41,12 @@ export default function Usage() {
} catch (error: any) {
toast.error(`Failed to load usage data: ${error.message}`);
} finally {
setLoading(false);
stopLoading();
}
};
if (loading) {
return (
<div className="p-6">
<PageMeta title="Usage" description="Monitor your credit usage" />
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading...</div>
</div>
</div>
);
}
return (
<div className="p-6">
<>
<PageMeta title="Usage" description="Monitor your credit usage and account limits" />
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Credit Usage & Activity</h1>
@@ -175,6 +165,6 @@ export default function Usage() {
</div>
</Card>
</div>
</div>
</>
);
}

View File

@@ -1,24 +0,0 @@
import PageMeta from "../components/common/PageMeta";
export default function Blank() {
return (
<div>
<PageMeta
title="React.js Blank Dashboard | TailAdmin - Next.js Admin Dashboard Template"
description="This is React.js Blank Dashboard page for TailAdmin - React.js Tailwind CSS Admin Dashboard Template"
/>
<div className="min-h-screen rounded-2xl border border-gray-200 bg-white px-5 py-7 dark:border-gray-800 dark:bg-white/[0.03] xl:px-10 xl:py-12">
<div className="mx-auto w-full max-w-[630px] text-center">
<h3 className="mb-4 font-semibold text-gray-800 text-theme-xl dark:text-white/90 sm:text-2xl">
Card Title Here
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400 sm:text-base">
Start putting content on grids or panels, you can also use different
combinations of grids.Please check out the dashboard and other pages
</p>
</div>
</div>
</div>
);
}

View File

@@ -1,285 +0,0 @@
import { useState, useRef, useEffect } from "react";
import FullCalendar from "@fullcalendar/react";
import dayGridPlugin from "@fullcalendar/daygrid";
import timeGridPlugin from "@fullcalendar/timegrid";
import interactionPlugin from "@fullcalendar/interaction";
import { EventInput, DateSelectArg, EventClickArg } from "@fullcalendar/core";
import { Modal } from "../components/ui/modal";
import { useModal } from "../hooks/useModal";
import PageMeta from "../components/common/PageMeta";
import Button from "../components/ui/button/Button";
import InputField from "../components/form/input/InputField";
interface CalendarEvent extends EventInput {
extendedProps: {
calendar: string;
};
}
const Calendar: React.FC = () => {
const [selectedEvent, setSelectedEvent] = useState<CalendarEvent | null>(
null
);
const [eventTitle, setEventTitle] = useState("");
const [eventStartDate, setEventStartDate] = useState("");
const [eventEndDate, setEventEndDate] = useState("");
const [eventLevel, setEventLevel] = useState("");
const [events, setEvents] = useState<CalendarEvent[]>([]);
const calendarRef = useRef<FullCalendar>(null);
const { isOpen, openModal, closeModal } = useModal();
const calendarsEvents = {
Danger: "danger",
Success: "success",
Primary: "primary",
Warning: "warning",
};
useEffect(() => {
// Initialize with some events
setEvents([
{
id: "1",
title: "Event Conf.",
start: new Date().toISOString().split("T")[0],
extendedProps: { calendar: "Danger" },
},
{
id: "2",
title: "Meeting",
start: new Date(Date.now() + 86400000).toISOString().split("T")[0],
extendedProps: { calendar: "Success" },
},
{
id: "3",
title: "Workshop",
start: new Date(Date.now() + 172800000).toISOString().split("T")[0],
end: new Date(Date.now() + 259200000).toISOString().split("T")[0],
extendedProps: { calendar: "Primary" },
},
]);
}, []);
const handleDateSelect = (selectInfo: DateSelectArg) => {
resetModalFields();
setEventStartDate(selectInfo.startStr);
setEventEndDate(selectInfo.endStr || selectInfo.startStr);
openModal();
};
const handleEventClick = (clickInfo: EventClickArg) => {
const event = clickInfo.event;
setSelectedEvent(event as unknown as CalendarEvent);
setEventTitle(event.title);
setEventStartDate(event.start?.toISOString().split("T")[0] || "");
setEventEndDate(event.end?.toISOString().split("T")[0] || "");
setEventLevel(event.extendedProps.calendar);
openModal();
};
const handleAddOrUpdateEvent = () => {
if (selectedEvent) {
// Update existing event
setEvents((prevEvents) =>
prevEvents.map((event) =>
event.id === selectedEvent.id
? {
...event,
title: eventTitle,
start: eventStartDate,
end: eventEndDate,
extendedProps: { calendar: eventLevel },
}
: event
)
);
} else {
// Add new event
const newEvent: CalendarEvent = {
id: Date.now().toString(),
title: eventTitle,
start: eventStartDate,
end: eventEndDate,
allDay: true,
extendedProps: { calendar: eventLevel },
};
setEvents((prevEvents) => [...prevEvents, newEvent]);
}
closeModal();
resetModalFields();
};
const resetModalFields = () => {
setEventTitle("");
setEventStartDate("");
setEventEndDate("");
setEventLevel("");
setSelectedEvent(null);
};
return (
<>
<PageMeta
title="React.js Calendar Dashboard | TailAdmin - Next.js Admin Dashboard Template"
description="This is React.js Calendar Dashboard page for TailAdmin - React.js Tailwind CSS Admin Dashboard Template"
/>
<div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<div className="custom-calendar">
<FullCalendar
ref={calendarRef}
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
initialView="dayGridMonth"
headerToolbar={{
left: "prev,next addEventButton",
center: "title",
right: "dayGridMonth,timeGridWeek,timeGridDay",
}}
events={events}
selectable={true}
select={handleDateSelect}
eventClick={handleEventClick}
eventContent={renderEventContent}
customButtons={{
addEventButton: {
text: "Add Event +",
click: openModal,
},
}}
/>
</div>
<Modal
isOpen={isOpen}
onClose={closeModal}
className="max-w-[700px] p-6 lg:p-10"
>
<div className="flex flex-col px-2 overflow-y-auto custom-scrollbar">
<div>
<h5 className="mb-2 font-semibold text-gray-800 modal-title text-theme-xl dark:text-white/90 lg:text-2xl">
{selectedEvent ? "Edit Event" : "Add Event"}
</h5>
<p className="text-sm text-gray-500 dark:text-gray-400">
Plan your next big moment: schedule or edit an event to stay on
track
</p>
</div>
<div className="mt-8">
<div>
<div>
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
Event Title
</label>
<InputField
id="event-title"
type="text"
value={eventTitle}
onChange={(e) => setEventTitle(e.target.value)}
/>
</div>
</div>
<div className="mt-6">
<label className="block mb-4 text-sm font-medium text-gray-700 dark:text-gray-400">
Event Color
</label>
<div className="flex flex-wrap items-center gap-4 sm:gap-5">
{Object.entries(calendarsEvents).map(([key, value]) => (
<div key={key} className="n-chk">
<div
className={`form-check form-check-${value} form-check-inline`}
>
<label
className="flex items-center text-sm text-gray-700 form-check-label dark:text-gray-400"
htmlFor={`modal${key}`}
>
<span className="relative">
<input
className="sr-only form-check-input"
type="radio"
name="event-level"
value={key}
id={`modal${key}`}
checked={eventLevel === key}
onChange={() => setEventLevel(key)}
/>
<span className="flex items-center justify-center w-5 h-5 mr-2 border border-gray-300 rounded-full box dark:border-gray-700">
<span
className={`h-2 w-2 rounded-full bg-white ${
eventLevel === key ? "block" : "hidden"
}`}
></span>
</span>
</span>
{key}
</label>
</div>
</div>
))}
</div>
</div>
<div className="mt-6">
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
Enter Start Date
</label>
<div className="relative">
<InputField
id="event-start-date"
type="date"
value={eventStartDate}
onChange={(e) => setEventStartDate(e.target.value)}
/>
</div>
</div>
<div className="mt-6">
<label className="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
Enter End Date
</label>
<div className="relative">
<InputField
id="event-end-date"
type="date"
value={eventEndDate}
onChange={(e) => setEventEndDate(e.target.value)}
/>
</div>
</div>
</div>
<div className="flex items-center gap-3 mt-6 modal-footer sm:justify-end">
<Button
onClick={closeModal}
variant="outline"
tone="neutral"
size="md"
>
Close
</Button>
<Button
onClick={handleAddOrUpdateEvent}
variant="primary"
tone="brand"
size="md"
>
{selectedEvent ? "Update Changes" : "Add Event"}
</Button>
</div>
</div>
</Modal>
</div>
</>
);
};
const renderEventContent = (eventInfo: any) => {
const colorClass = `fc-bg-${eventInfo.event.extendedProps.calendar.toLowerCase()}`;
return (
<div
className={`event-fc-color flex fc-event-main ${colorClass} p-1 rounded-sm`}
>
<div className="fc-daygrid-event-dot"></div>
<div className="fc-event-time">{eventInfo.timeText}</div>
<div className="fc-event-title">{eventInfo.event.title}</div>
</div>
);
};
export default Calendar;

View File

@@ -1,19 +0,0 @@
import ComponentCard from "../../components/common/ComponentCard";
import BarChartOne from "../../components/charts/bar/BarChartOne";
import PageMeta from "../../components/common/PageMeta";
export default function BarChart() {
return (
<div>
<PageMeta
title="React.js Chart Dashboard | TailAdmin - React.js Admin Dashboard Template"
description="This is React.js Chart Dashboard page for TailAdmin - React.js Tailwind CSS Admin Dashboard Template"
/>
<div className="space-y-6">
<ComponentCard title="Bar Chart 1">
<BarChartOne />
</ComponentCard>
</div>
</div>
);
}

View File

@@ -1,19 +0,0 @@
import ComponentCard from "../../components/common/ComponentCard";
import LineChartOne from "../../components/charts/line/LineChartOne";
import PageMeta from "../../components/common/PageMeta";
export default function LineChart() {
return (
<>
<PageMeta
title="React.js Chart Dashboard | TailAdmin - React.js Admin Dashboard Template"
description="This is React.js Chart Dashboard page for TailAdmin - React.js Tailwind CSS Admin Dashboard Template"
/>
<div className="space-y-6">
<ComponentCard title="Line Chart 1">
<LineChartOne />
</ComponentCard>
</div>
</>
);
}

View File

@@ -1,38 +0,0 @@
import DefaultInputs from "../../components/form/form-elements/DefaultInputs";
import InputGroup from "../../components/form/form-elements/InputGroup";
import DropzoneComponent from "../../components/form/form-elements/DropZone";
import CheckboxComponents from "../../components/form/form-elements/CheckboxComponents";
import RadioButtons from "../../components/form/form-elements/RadioButtons";
import ToggleSwitch from "../../components/form/form-elements/ToggleSwitch";
import FileInputExample from "../../components/form/form-elements/FileInputExample";
import SelectInputs from "../../components/form/form-elements/SelectInputs";
import TextAreaInput from "../../components/form/form-elements/TextAreaInput";
import InputStates from "../../components/form/form-elements/InputStates";
import PageMeta from "../../components/common/PageMeta";
export default function FormElements() {
return (
<div>
<PageMeta
title="React.js Form Elements Dashboard | TailAdmin - React.js Admin Dashboard Template"
description="This is React.js Form Elements Dashboard page for TailAdmin - React.js Tailwind CSS Admin Dashboard Template"
/>
<div className="grid grid-cols-1 gap-6 xl:grid-cols-2">
<div className="space-y-6">
<DefaultInputs />
<SelectInputs />
<TextAreaInput />
<InputStates />
</div>
<div className="space-y-6">
<InputGroup />
<FileInputExample />
<CheckboxComponents />
<RadioButtons />
<ToggleSwitch />
<DropzoneComponent />
</div>
</div>
</div>
);
}

View File

@@ -12,22 +12,23 @@ import { LinkResults } from '../../components/linker/LinkResults';
import { PlugInIcon, CheckCircleIcon, FileIcon } from '../../icons';
import { useSectorStore } from '../../store/sectorStore';
import { usePageSizeStore } from '../../store/pageSizeStore';
import { usePageLoading } from '../../context/PageLoadingContext';
export default function LinkerContentList() {
const navigate = useNavigate();
const toast = useToast();
const { activeSector } = useSectorStore();
const { pageSize } = usePageSizeStore();
const { startLoading, stopLoading } = usePageLoading();
const [content, setContent] = useState<ContentType[]>([]);
const [loading, setLoading] = useState(true);
const [processing, setProcessing] = useState<number | null>(null);
const [linkResults, setLinkResults] = useState<Record<number, any>>({});
const [currentPage, setCurrentPage] = useState(1);
const [totalCount, setTotalCount] = useState(0);
const loadContent = useCallback(async () => {
setLoading(true);
startLoading('Loading content...');
try {
const data = await fetchContent({
page: currentPage,
@@ -40,9 +41,9 @@ export default function LinkerContentList() {
console.error('Error loading content:', error);
toast.error(`Failed to load content: ${error.message}`);
} finally {
setLoading(false);
stopLoading();
}
}, [currentPage, pageSize, activeSector, toast]);
}, [currentPage, pageSize, activeSector, toast, startLoading, stopLoading]);
useEffect(() => {
loadContent();
@@ -109,12 +110,6 @@ export default function LinkerContentList() {
]} />}
/>
{loading ? (
<div className="text-center py-12">
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-brand-500"></div>
<p className="mt-2 text-gray-600 dark:text-gray-400">Loading content...</p>
</div>
) : (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
@@ -233,7 +228,6 @@ export default function LinkerContentList() {
</div>
)}
</div>
)}
{/* Module footer placeholder - module on hold */}
</div>

View File

@@ -1,170 +0,0 @@
import { useEffect, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import PageMeta from '../../components/common/PageMeta';
import ComponentCard from '../../components/common/ComponentCard';
import EnhancedMetricCard from '../../components/dashboard/EnhancedMetricCard';
import PageHeader from '../../components/common/PageHeader';
import { FileTextIcon, ArrowRightIcon, PlugInIcon, ArrowUpIcon } from '../../icons';
import { fetchContent } from '../../services/api';
import { useSiteStore } from '../../store/siteStore';
import { useSectorStore } from '../../store/sectorStore';
interface LinkerStats {
totalLinked: number;
totalLinks: number;
averageLinksPerContent: number;
contentWithLinks: number;
contentWithoutLinks: number;
}
export default function LinkerDashboard() {
const navigate = useNavigate();
const { activeSite } = useSiteStore();
const { activeSector } = useSectorStore();
const [stats, setStats] = useState<LinkerStats | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchDashboardData();
}, [activeSite, activeSector]);
const fetchDashboardData = async () => {
try {
setLoading(true);
// Fetch content to calculate stats
const contentRes = await fetchContent({
page_size: 1000,
sector_id: activeSector?.id,
});
const content = contentRes.results || [];
// Calculate stats
const contentWithLinks = content.filter(c => c.internal_links && c.internal_links.length > 0);
const totalLinks = content.reduce((sum, c) => sum + (c.internal_links?.length || 0), 0);
const averageLinksPerContent = contentWithLinks.length > 0
? (totalLinks / contentWithLinks.length)
: 0;
setStats({
totalLinked: contentWithLinks.length,
totalLinks,
averageLinksPerContent: parseFloat(averageLinksPerContent.toFixed(1)),
contentWithLinks: contentWithLinks.length,
contentWithoutLinks: content.length - contentWithLinks.length,
});
} catch (error: any) {
console.error('Error loading linker stats:', error);
} finally {
setLoading(false);
}
};
return (
<>
<PageMeta title="Internal Linking Dashboard" description="Track your internal linking progress" />
<div className="space-y-6">
<div className="flex items-center justify-between mb-6">
<PageHeader
title="Internal Linking Dashboard"
lastUpdated={new Date()}
badge={{
icon: <PlugInIcon />,
color: 'blue',
}}
/>
<Link
to="/linker/content"
className="inline-flex items-center gap-2 px-4 py-2 bg-brand-500 text-white rounded-lg hover:bg-brand-600 transition-colors"
>
<PlugInIcon />
View Content
</Link>
</div>
<p className="text-gray-600 dark:text-gray-400 mb-6">
Manage internal linking for your content
</p>
{loading ? (
<div className="text-center py-12">
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-brand-500"></div>
<p className="mt-2 text-gray-600 dark:text-gray-400">Loading stats...</p>
</div>
) : stats ? (
<>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<EnhancedMetricCard
title="Total Linked"
value={stats.totalLinked.toString()}
subtitle={`${stats.contentWithoutLinks} without links`}
icon={<FileTextIcon className="w-6 h-6" />}
accentColor="blue"
onClick={() => navigate('/linker/content')}
/>
<EnhancedMetricCard
title="Total Links"
value={stats.totalLinks.toString()}
subtitle="Internal links created"
icon={<PlugInIcon className="w-6 h-6" />}
accentColor="purple"
onClick={() => navigate('/linker/content')}
/>
<EnhancedMetricCard
title="Avg Links/Content"
value={stats.averageLinksPerContent.toString()}
subtitle="Average per linked content"
icon={<ArrowUpIcon className="w-6 h-6" />}
accentColor="green"
onClick={() => navigate('/linker/content')}
/>
</div>
{/* Quick Actions */}
<ComponentCard title="Quick Actions" className="mt-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Link
to="/linker/content"
className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
>
<div className="flex items-center gap-3">
<PlugInIcon className="w-5 h-5 text-brand-500" />
<div>
<h3 className="font-medium text-gray-900 dark:text-white">Link Content</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">Process content for internal linking</p>
</div>
</div>
<ArrowRightIcon className="w-5 h-5 text-gray-400" />
</Link>
<Link
to="/writer/content"
className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
>
<div className="flex items-center gap-3">
<FileTextIcon className="w-5 h-5 text-purple-500" />
<div>
<h3 className="font-medium text-gray-900 dark:text-white">View Content</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">Browse all content items</p>
</div>
</div>
<ArrowRightIcon className="w-5 h-5 text-gray-400" />
</Link>
</div>
</ComponentCard>
</>
) : (
<div className="text-center py-12">
<p className="text-gray-600 dark:text-gray-400">No data available</p>
</div>
)}
</div>
</>
);
}

View File

@@ -1,172 +0,0 @@
import { useEffect, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import PageMeta from '../../components/common/PageMeta';
import ComponentCard from '../../components/common/ComponentCard';
import EnhancedMetricCard from '../../components/dashboard/EnhancedMetricCard';
import PageHeader from '../../components/common/PageHeader';
import { BoltIcon, FileTextIcon, ArrowUpIcon, ArrowRightIcon } from '../../icons';
import { fetchContent } from '../../services/api';
import { useSiteStore } from '../../store/siteStore';
import { useSectorStore } from '../../store/sectorStore';
interface OptimizerStats {
totalOptimized: number;
averageScoreImprovement: number;
totalOperations: number;
contentWithScores: number;
contentWithoutScores: number;
}
export default function OptimizerDashboard() {
const navigate = useNavigate();
const { activeSite } = useSiteStore();
const { activeSector } = useSectorStore();
const [stats, setStats] = useState<OptimizerStats | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchDashboardData();
}, [activeSite, activeSector]);
const fetchDashboardData = async () => {
try {
setLoading(true);
// Fetch content to calculate stats
const contentRes = await fetchContent({
page_size: 1000,
sector_id: activeSector?.id,
});
const content = contentRes.results || [];
// Calculate stats
const contentWithScores = content.filter(
c => c.optimization_scores && c.optimization_scores.overall_score
);
const totalOptimized = content.filter(c => c.optimizer_version > 0).length;
// Calculate average improvement (simplified - would need optimization tasks for real data)
const averageScoreImprovement = contentWithScores.length > 0 ? 15.5 : 0;
setStats({
totalOptimized,
averageScoreImprovement: parseFloat(averageScoreImprovement.toFixed(1)),
totalOperations: 0, // Would need to fetch from optimization tasks
contentWithScores: contentWithScores.length,
contentWithoutScores: content.length - contentWithScores.length,
});
} catch (error: any) {
console.error('Error loading optimizer stats:', error);
} finally {
setLoading(false);
}
};
return (
<>
<PageMeta title="Optimization Dashboard" description="Track your content optimization progress" />
<div className="space-y-6">
<div className="flex items-center justify-between mb-6">
<PageHeader
title="Optimization Dashboard"
lastUpdated={new Date()}
badge={{
icon: <BoltIcon />,
color: 'orange',
}}
/>
<Link
to="/optimizer/content"
className="inline-flex items-center gap-2 px-4 py-2 bg-brand-500 text-white rounded-lg hover:bg-brand-600 transition-colors"
>
<BoltIcon />
Optimize Content
</Link>
</div>
<p className="text-gray-600 dark:text-gray-400 mb-6">
Optimize your content for SEO, readability, and engagement
</p>
{loading ? (
<div className="text-center py-12">
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-brand-500"></div>
<p className="mt-2 text-gray-600 dark:text-gray-400">Loading stats...</p>
</div>
) : stats ? (
<>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<EnhancedMetricCard
title="Total Optimized"
value={stats.totalOptimized.toString()}
subtitle={`${stats.contentWithoutScores} not optimized`}
icon={<FileTextIcon className="w-6 h-6" />}
accentColor="blue"
onClick={() => navigate('/optimizer/content')}
/>
<EnhancedMetricCard
title="Avg Score Improvement"
value={`+${stats.averageScoreImprovement}%`}
subtitle="Average improvement per optimization"
icon={<ArrowUpIcon className="w-6 h-6" />}
accentColor="green"
onClick={() => navigate('/optimizer/content')}
/>
<EnhancedMetricCard
title="Optimizations"
value={stats.totalOperations.toString()}
subtitle="Total optimization runs"
icon={<BoltIcon className="w-6 h-6" />}
accentColor="orange"
onClick={() => navigate('/optimizer/content')}
/>
</div>
{/* Quick Actions */}
<ComponentCard title="Quick Actions" className="mt-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Link
to="/optimizer/content"
className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
>
<div className="flex items-center gap-3">
<BoltIcon className="w-5 h-5 text-warning-500" />
<div>
<h3 className="font-medium text-gray-900 dark:text-white">Optimize Content</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">Select and optimize content items</p>
</div>
</div>
<ArrowRightIcon className="w-5 h-5 text-gray-400" />
</Link>
<Link
to="/writer/content"
className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
>
<div className="flex items-center gap-3">
<FileTextIcon className="w-5 h-5 text-purple-500" />
<div>
<h3 className="font-medium text-gray-900 dark:text-white">View Content</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">Browse all content items</p>
</div>
</div>
<ArrowRightIcon className="w-5 h-5 text-gray-400" />
</Link>
</div>
</ComponentCard>
</>
) : (
<div className="text-center py-12">
<p className="text-gray-600 dark:text-gray-400">No data available</p>
</div>
)}
</div>
</>
);
}

View File

@@ -121,18 +121,18 @@ export default function ClusterDetail() {
if (loading) {
return (
<div className="p-6">
<>
<PageMeta title="Cluster Details" description="Loading cluster information" />
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading cluster...</div>
</div>
</div>
</>
);
}
if (!cluster) {
return (
<div className="p-6">
<>
<PageMeta title="Cluster Not Found" description="The requested cluster could not be found" />
<Card className="p-12 text-center">
<p className="text-gray-600 dark:text-gray-400 mb-4">Cluster not found</p>
@@ -140,12 +140,12 @@ export default function ClusterDetail() {
Back to Clusters
</Button>
</Card>
</div>
</>
);
}
return (
<div className="p-6">
<>
<PageMeta
title={`${cluster.name} - Cluster Details - IGNY8`}
description={cluster.description || `View details for cluster: ${cluster.name}`}
@@ -366,6 +366,6 @@ export default function ClusterDetail() {
</div>
</Card>
)}
</div>
</>
);
}

View File

@@ -1,778 +0,0 @@
import { useEffect, useState, useMemo, lazy, Suspense } from "react";
import { Link, useNavigate } from "react-router-dom";
import PageMeta from "../../components/common/PageMeta";
import ComponentCard from "../../components/common/ComponentCard";
import { ProgressBar } from "../../components/ui/progress";
import { ApexOptions } from "apexcharts";
import EnhancedMetricCard from "../../components/dashboard/EnhancedMetricCard";
import PageHeader from "../../components/common/PageHeader";
const Chart = lazy(() => import("react-apexcharts").then((mod) => ({ default: mod.default })));
import {
ListIcon,
GroupIcon,
BoltIcon,
PieChartIcon,
ArrowRightIcon,
CheckCircleIcon,
TimeIcon,
ArrowUpIcon,
ArrowDownIcon,
PlugInIcon,
ClockIcon,
} from "../../icons";
import {
fetchKeywords,
fetchClusters,
fetchContentIdeas,
fetchTasks,
// fetchSiteBlueprints,
// SiteBlueprint,
} from "../../services/api";
import { useSiteStore } from "../../store/siteStore";
import { useSectorStore } from "../../store/sectorStore";
interface DashboardStats {
keywords: {
total: number;
mapped: number;
unmapped: number;
byStatus: Record<string, number>;
byCountry: Record<string, number>;
};
clusters: {
total: number;
withIdeas: number;
withoutIdeas: number;
totalVolume: number;
avgKeywords: number;
topClusters: Array<{ id: number; name: string; volume: number; keywords_count: number }>;
};
ideas: {
total: number;
queued: number;
notQueued: number;
byStatus: Record<string, number>;
byContentType: Record<string, number>;
};
workflow: {
keywordsReady: boolean;
clustersBuilt: boolean;
ideasGenerated: boolean;
readyForWriter: boolean;
};
}
export default function PlannerDashboard() {
const navigate = useNavigate();
const { activeSite } = useSiteStore();
const { activeSector } = useSectorStore();
const [stats, setStats] = useState<DashboardStats | null>(null);
const [loading, setLoading] = useState(true);
const [lastUpdated, setLastUpdated] = useState<Date>(new Date());
// Fetch real data
const fetchDashboardData = async () => {
try {
setLoading(true);
const [keywordsRes, clustersRes, ideasRes, tasksRes] = await Promise.all([
fetchKeywords({ page_size: 1000, sector_id: activeSector?.id }),
fetchClusters({ page_size: 1000, sector_id: activeSector?.id }),
fetchContentIdeas({ page_size: 1000, sector_id: activeSector?.id }),
fetchTasks({ page_size: 1000, sector_id: activeSector?.id })
]);
const keywords = keywordsRes.results || [];
const mappedKeywords = keywords.filter(k => k.cluster && k.cluster.length > 0);
const unmappedKeywords = keywords.filter(k => !k.cluster || k.cluster.length === 0);
const keywordsByStatus: Record<string, number> = {};
const keywordsByCountry: Record<string, number> = {};
keywords.forEach(k => {
keywordsByStatus[k.status || 'unknown'] = (keywordsByStatus[k.status || 'unknown'] || 0) + 1;
if (k.country) {
keywordsByCountry[k.country] = (keywordsByCountry[k.country] || 0) + 1;
}
});
const clusters = clustersRes.results || [];
const clustersWithIdeas = clusters.filter(c => c.keywords_count > 0);
const totalVolume = clusters.reduce((sum, c) => sum + (c.volume || 0), 0);
const totalKeywordsInClusters = clusters.reduce((sum, c) => sum + (c.keywords_count || 0), 0);
const avgKeywords = clusters.length > 0 ? Math.round(totalKeywordsInClusters / clusters.length) : 0;
const topClusters = [...clusters]
.sort((a, b) => (b.volume || 0) - (a.volume || 0))
.slice(0, 5)
.map(c => ({
id: c.id,
name: c.name || 'Unnamed Cluster',
volume: c.volume || 0,
keywords_count: c.keywords_count || 0
}));
const ideas = ideasRes.results || [];
const ideaIds = new Set(ideas.map(i => i.id));
const tasks = tasksRes.results || [];
const queuedIdeas = tasks.filter(t => t.idea && ideaIds.has(t.idea)).length;
const notQueuedIdeas = ideas.length - queuedIdeas;
const ideasByStatus: Record<string, number> = {};
const ideasByContentType: Record<string, number> = {};
ideas.forEach(i => {
ideasByStatus[i.status || 'new'] = (ideasByStatus[i.status || 'new'] || 0) + 1;
if (i.content_type) {
ideasByContentType[i.content_type] = (ideasByContentType[i.content_type] || 0) + 1;
}
});
setStats({
keywords: {
total: keywords.length,
mapped: mappedKeywords.length,
unmapped: unmappedKeywords.length,
byStatus: keywordsByStatus,
byCountry: keywordsByCountry
},
clusters: {
total: clusters.length,
withIdeas: clustersWithIdeas.length,
withoutIdeas: clusters.length - clustersWithIdeas.length,
totalVolume,
avgKeywords,
topClusters
},
ideas: {
total: ideas.length,
queued: queuedIdeas,
notQueued: notQueuedIdeas,
byStatus: ideasByStatus,
byContentType: ideasByContentType
},
workflow: {
keywordsReady: keywords.length > 0,
clustersBuilt: clusters.length > 0,
ideasGenerated: ideas.length > 0,
readyForWriter: queuedIdeas > 0
}
});
setLastUpdated(new Date());
} catch (error) {
console.error('Error fetching dashboard data:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchDashboardData();
const interval = setInterval(fetchDashboardData, 30000);
return () => clearInterval(interval);
}, [activeSector?.id, activeSite?.id]);
const keywordMappingPct = useMemo(() => {
if (!stats || stats.keywords.total === 0) return 0;
return Math.round((stats.keywords.mapped / stats.keywords.total) * 100);
}, [stats]);
const clustersIdeasPct = useMemo(() => {
if (!stats || stats.clusters.total === 0) return 0;
return Math.round((stats.clusters.withIdeas / stats.clusters.total) * 100);
}, [stats]);
const ideasQueuedPct = useMemo(() => {
if (!stats || stats.ideas.total === 0) return 0;
return Math.round((stats.ideas.queued / stats.ideas.total) * 100);
}, [stats]);
const plannerModules = [
{
title: "Keywords",
description: "Manage and discover keywords",
icon: ListIcon,
color: "from-[var(--color-primary)] to-[var(--color-primary-dark)]",
path: "/planner/keywords",
count: stats?.keywords.total || 0,
metric: `${stats?.keywords.mapped || 0} mapped`,
},
{
title: "Clusters",
description: "Keyword clusters and groups",
icon: GroupIcon,
color: "from-[var(--color-success)] to-[var(--color-success-dark)]",
path: "/planner/clusters",
count: stats?.clusters.total || 0,
metric: `${stats?.clusters.totalVolume.toLocaleString() || 0} volume`,
},
{
title: "Ideas",
description: "Content ideas and concepts",
icon: BoltIcon,
color: "from-[var(--color-warning)] to-[var(--color-warning-dark)]",
path: "/planner/ideas",
count: stats?.ideas.total || 0,
metric: `${stats?.ideas.queued || 0} queued`,
},
{
title: "Keyword Opportunities",
description: "Discover new keyword opportunities",
icon: PieChartIcon,
color: "from-[var(--color-purple)] to-[var(--color-purple-dark)]",
path: "/planner/keyword-opportunities",
count: 0,
metric: "Discover new keywords",
},
];
const recentActivity = [
{
id: 1,
type: "Keywords Clustered",
description: `${stats?.clusters.total || 0} new clusters created`,
timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000),
icon: GroupIcon,
color: "text-success-600",
},
{
id: 2,
type: "Ideas Generated",
description: `${stats?.ideas.total || 0} content ideas created`,
timestamp: new Date(Date.now() - 4 * 60 * 60 * 1000),
icon: BoltIcon,
color: "text-warning-600",
},
{
id: 3,
type: "Keywords Added",
description: `${stats?.keywords.total || 0} keywords in database`,
timestamp: new Date(Date.now() - 6 * 60 * 60 * 1000),
icon: ListIcon,
color: "text-brand-600",
},
];
const chartOptions: ApexOptions = {
chart: {
type: "area",
height: 300,
toolbar: { show: false },
zoom: { enabled: false },
},
stroke: {
curve: "smooth",
width: 3,
},
xaxis: {
categories: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
labels: { style: { colors: "var(--color-gray-500)" } },
},
yaxis: {
labels: { style: { colors: "var(--color-gray-500)" } },
},
legend: {
position: "top",
labels: { colors: "var(--color-gray-500)" },
},
colors: ["var(--color-primary)", "var(--color-success)", "var(--color-warning)"],
grid: {
borderColor: "var(--color-gray-200)",
},
fill: {
type: "gradient",
gradient: {
opacityFrom: 0.6,
opacityTo: 0.1,
},
},
};
const chartSeries = [
{
name: "Keywords Added",
data: [12, 19, 15, 25, 22, 18, 24],
},
{
name: "Clusters Created",
data: [8, 12, 10, 15, 14, 11, 16],
},
{
name: "Ideas Generated",
data: [5, 8, 6, 10, 9, 7, 11],
},
];
const keywordsStatusChart = useMemo(() => {
if (!stats) return null;
const options: ApexOptions = {
chart: {
type: 'donut',
fontFamily: 'Outfit, sans-serif',
toolbar: { show: false }
},
labels: Object.keys(stats.keywords.byStatus).filter(key => stats.keywords.byStatus[key] > 0),
colors: ['var(--color-primary)', 'var(--color-success)', 'var(--color-warning)', 'var(--color-danger)', 'var(--color-purple)'],
legend: {
position: 'bottom',
fontFamily: 'Outfit',
show: true
},
dataLabels: {
enabled: false
},
plotOptions: {
pie: {
donut: {
size: '70%',
labels: {
show: true,
name: { show: false },
value: {
show: true,
fontSize: '24px',
fontWeight: 700,
color: 'var(--color-primary)',
fontFamily: 'Outfit',
formatter: () => {
const total = Object.values(stats.keywords.byStatus).reduce((a, b) => a + b, 0);
return total > 0 ? total.toString() : '0';
}
},
total: { show: false }
}
}
}
}
};
const series = Object.keys(stats.keywords.byStatus)
.filter(key => stats.keywords.byStatus[key] > 0)
.map(key => stats.keywords.byStatus[key]);
return { options, series };
}, [stats]);
const topClustersChart = useMemo(() => {
if (!stats || stats.clusters.topClusters.length === 0) return null;
const options: ApexOptions = {
chart: {
type: 'bar',
fontFamily: 'Outfit, sans-serif',
toolbar: { show: false },
height: 300
},
colors: ['var(--color-success)'],
plotOptions: {
bar: {
horizontal: true,
borderRadius: 5,
dataLabels: {
position: 'top'
}
}
},
dataLabels: {
enabled: true,
formatter: (val: number) => val.toLocaleString(),
offsetX: 10
},
xaxis: {
categories: stats.clusters.topClusters.map(c => c.name),
labels: {
style: {
fontFamily: 'Outfit',
fontSize: '12px'
}
}
},
yaxis: {
labels: {
style: {
fontFamily: 'Outfit'
}
}
},
tooltip: {
y: {
formatter: (val: number) => `${val.toLocaleString()} volume`
}
}
};
const series = [{
name: 'Search Volume',
data: stats.clusters.topClusters.map(c => c.volume)
}];
return { options, series };
}, [stats]);
const formatTimeAgo = (date: Date) => {
const minutes = Math.floor((Date.now() - date.getTime()) / 60000);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
return `${days}d ago`;
};
if (loading && !stats) {
return (
<>
<PageMeta title="Planner Dashboard - IGNY8" description="Content planning overview" />
<div className="flex items-center justify-center h-96">
<div className="text-center">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-4 border-brand-500 border-t-transparent"></div>
<p className="mt-4 text-gray-600 dark:text-gray-400">Loading dashboard data...</p>
</div>
</div>
</>
);
}
if (!stats && !loading) {
return (
<>
<PageMeta title="Planner Dashboard - IGNY8" description="Content planning overview" />
<div className="text-center py-12">
<p className="text-gray-600 dark:text-gray-400">
{activeSector ? 'No data available for the selected sector.' : 'No data available. Select a sector or wait for data to load.'}
</p>
</div>
</>
);
}
if (!stats) return null;
return (
<>
<PageMeta title="Planning Dashboard - IGNY8" description="Track your content planning progress" />
<PageHeader
title="Planning Dashboard"
lastUpdated={lastUpdated}
showRefresh={true}
onRefresh={fetchDashboardData}
/>
<div className="space-y-6">
{/* Key Metrics */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<EnhancedMetricCard
title="Total Keywords"
value={stats.keywords.total}
subtitle={`${stats.keywords.mapped} mapped • ${stats.keywords.unmapped} unmapped`}
icon={<ListIcon className="size-6" />}
accentColor="blue"
trend={0}
href="/planner/keywords"
/>
<EnhancedMetricCard
title="Clusters Built"
value={stats.clusters.total}
subtitle={`${stats.clusters.totalVolume.toLocaleString()} volume • ${stats.clusters.avgKeywords} avg keywords`}
icon={<GroupIcon className="size-6" />}
accentColor="green"
trend={0}
href="/planner/clusters"
/>
<EnhancedMetricCard
title="Ideas Generated"
value={stats.ideas.total}
subtitle={`${stats.ideas.queued} queued • ${stats.ideas.notQueued} not queued`}
icon={<BoltIcon className="size-6" />}
accentColor="orange"
trend={0}
href="/planner/ideas"
/>
<EnhancedMetricCard
title="Mapping Progress"
value={`${keywordMappingPct}%`}
subtitle={`${stats.keywords.mapped} of ${stats.keywords.total} keywords mapped`}
icon={<PieChartIcon className="size-6" />}
accentColor="purple"
trend={0}
href="/planner/keywords"
/>
</div>
{/* Planner Modules */}
<ComponentCard title="Planner Modules" desc="Access all planning tools and features">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{plannerModules.map((module) => {
const Icon = module.icon;
return (
<Link
key={module.title}
to={module.path}
className="rounded-2xl border-2 border-gray-200 bg-white p-6 hover:shadow-xl hover:-translate-y-1 transition-all group"
>
<div className="flex items-start justify-between mb-4">
<div className={`inline-flex size-14 rounded-xl bg-gradient-to-br ${module.color} items-center justify-center text-white shadow-lg`}>
<Icon className="h-7 w-7" />
</div>
</div>
<h3 className="text-lg font-bold text-gray-900 mb-2">{module.title}</h3>
<p className="text-sm text-gray-600 mb-4">{module.description}</p>
<div className="flex items-center justify-between">
<div>
<div className="text-2xl font-bold text-gray-900">{module.count}</div>
<div className="text-xs text-gray-500">{module.metric}</div>
</div>
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-primary)] group-hover:translate-x-1 transition" />
</div>
</Link>
);
})}
</div>
</ComponentCard>
{/* Activity Chart & Recent Activity */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<ComponentCard title="Planning Activity" desc="Keywords, clusters, and ideas over the past week">
<Suspense fallback={<div className="h-[300px] flex items-center justify-center">Loading chart...</div>}>
<Chart options={chartOptions} series={chartSeries} type="area" height={300} />
</Suspense>
</ComponentCard>
<ComponentCard title="Recent Activity" desc="Latest planning actions and updates">
<div className="space-y-4">
{recentActivity.map((activity) => {
const Icon = activity.icon;
return (
<div
key={activity.id}
className="flex items-center gap-4 p-4 rounded-lg border border-gray-200 bg-white hover:shadow-md transition"
>
<div className={`size-10 rounded-lg bg-gradient-to-br from-gray-100 to-gray-200 flex items-center justify-center ${activity.color}`}>
<Icon className="h-5 w-5" />
</div>
<div className="flex-1">
<div className="flex items-center justify-between mb-1">
<h4 className="font-semibold text-gray-900">{activity.type}</h4>
<span className="text-xs text-gray-500">{formatTimeAgo(activity.timestamp)}</span>
</div>
<p className="text-sm text-gray-600">{activity.description}</p>
</div>
</div>
);
})}
</div>
</ComponentCard>
</div>
{/* Charts */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{keywordsStatusChart && (
<ComponentCard title="Keywords by Status" desc="Distribution of keywords across statuses">
<Suspense fallback={<div className="h-[300px] flex items-center justify-center">Loading chart...</div>}>
<Chart
options={keywordsStatusChart.options}
series={keywordsStatusChart.series}
type="donut"
height={300}
/>
</Suspense>
</ComponentCard>
)}
{topClustersChart && (
<ComponentCard title="Top 5 Clusters by Volume" desc="Highest volume keyword clusters">
<Suspense fallback={<div className="h-[300px] flex items-center justify-center">Loading chart...</div>}>
<Chart
options={topClustersChart.options}
series={topClustersChart.series}
type="bar"
height={300}
/>
</Suspense>
</ComponentCard>
)}
</div>
{/* Progress Summary */}
<ComponentCard title="Planning Progress" desc="Track your planning workflow progress">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Keyword Mapping</span>
<span className="text-sm font-semibold text-gray-800 dark:text-white/90">{keywordMappingPct}%</span>
</div>
<ProgressBar value={keywordMappingPct} color="primary" size="md" />
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{stats.keywords.mapped} of {stats.keywords.total} keywords mapped
</p>
</div>
<div>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Clusters With Ideas</span>
<span className="text-sm font-semibold text-gray-800 dark:text-white/90">{clustersIdeasPct}%</span>
</div>
<ProgressBar value={clustersIdeasPct} color="success" size="md" />
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{stats.clusters.withIdeas} of {stats.clusters.total} clusters have ideas
</p>
</div>
<div>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Ideas Queued to Writer</span>
<span className="text-sm font-semibold text-gray-800 dark:text-white/90">{ideasQueuedPct}%</span>
</div>
<ProgressBar value={ideasQueuedPct} color="warning" size="md" />
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{stats.ideas.queued} of {stats.ideas.total} ideas queued
</p>
</div>
</div>
</ComponentCard>
{/* Quick Actions */}
<ComponentCard title="Quick Actions" desc="Common planning tasks and shortcuts">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Link
to="/planner/keyword-opportunities"
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"
>
<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">
<ListIcon className="h-6 w-6" />
</div>
<div className="flex-1 text-left">
<h4 className="font-semibold text-gray-900 mb-1">Add Keywords</h4>
<p className="text-sm text-gray-600">Discover opportunities</p>
</div>
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-primary)] transition" />
</Link>
<Link
to="/planner/clusters"
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-success-500 hover:shadow-lg transition-all group"
>
<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">
<GroupIcon className="h-6 w-6" />
</div>
<div className="flex-1 text-left">
<h4 className="font-semibold text-gray-900 mb-1">Auto Cluster</h4>
<p className="text-sm text-gray-600">Group keywords</p>
</div>
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-success-500 transition" />
</Link>
<Link
to="/planner/ideas"
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-warning-500 hover:shadow-lg transition-all group"
>
<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">Generate Ideas</h4>
<p className="text-sm text-gray-600">Create content ideas</p>
</div>
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-warning-500 transition" />
</Link>
<Link
to="/automation"
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-purple-500 hover:shadow-lg transition-all group"
>
<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">Setup Automation</h4>
<p className="text-sm text-gray-600">Automate workflows</p>
</div>
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-purple-500 transition" />
</Link>
</div>
</ComponentCard>
{/* Info Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<ComponentCard title="How Planner Works" desc="Understanding the planning workflow">
<div className="space-y-4">
<div className="flex gap-4">
<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">
<ListIcon className="h-5 w-5" />
</div>
<div>
<h4 className="font-semibold text-gray-900 mb-1">Keyword Discovery</h4>
<p className="text-sm text-gray-600">
Discover high-volume keywords from our global database. Add keywords manually or import from keyword opportunities.
</p>
</div>
</div>
<div className="flex gap-4">
<div className="flex-shrink-0 size-10 rounded-lg bg-gradient-to-br from-[var(--color-success)] to-[var(--color-success-dark)] flex items-center justify-center text-white shadow-md">
<GroupIcon className="h-5 w-5" />
</div>
<div>
<h4 className="font-semibold text-gray-900 mb-1">AI Clustering</h4>
<p className="text-sm text-gray-600">
Automatically group related keywords into strategic clusters. Each cluster represents a content topic with shared search intent.
</p>
</div>
</div>
<div className="flex gap-4">
<div className="flex-shrink-0 size-10 rounded-lg bg-gradient-to-br from-[var(--color-warning)] to-[var(--color-warning-dark)] flex items-center justify-center text-white shadow-md">
<BoltIcon className="h-5 w-5" />
</div>
<div>
<h4 className="font-semibold text-gray-900 mb-1">Idea Generation</h4>
<p className="text-sm text-gray-600">
Generate content ideas from clusters using AI. Each idea includes title, outline, and target keywords for content creation.
</p>
</div>
</div>
</div>
</ComponentCard>
<ComponentCard title="Getting Started" desc="Quick guide to using Planner">
<div className="space-y-3">
<div className="flex items-start gap-3">
<div className="flex-shrink-0 size-8 rounded-full bg-brand-500 text-white flex items-center justify-center font-bold text-sm">
1
</div>
<div>
<h4 className="font-semibold text-gray-900 mb-1">Add Keywords</h4>
<p className="text-sm text-gray-600">
Start by adding keywords from the keyword opportunities page. You can search by volume, difficulty, or intent.
</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="flex-shrink-0 size-8 rounded-full bg-success-500 text-white flex items-center justify-center font-bold text-sm">
2
</div>
<div>
<h4 className="font-semibold text-gray-900 mb-1">Cluster Keywords</h4>
<p className="text-sm text-gray-600">
Use the auto-cluster feature to group related keywords. Review and refine clusters to match your content strategy.
</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="flex-shrink-0 size-8 rounded-full bg-warning-500 text-white flex items-center justify-center font-bold text-sm">
3
</div>
<div>
<h4 className="font-semibold text-gray-900 mb-1">Generate Ideas</h4>
<p className="text-sm text-gray-600">
Create content ideas from your clusters. Queue ideas to the Writer module to start content creation.
</p>
</div>
</div>
</div>
</ComponentCard>
</div>
</div>
</>
);
}

View File

@@ -1,27 +0,0 @@
import PageMeta from "../../components/common/PageMeta";
import ComponentCard from "../../components/common/ComponentCard";
import PageHeader from "../../components/common/PageHeader";
import { PieChartIcon } from "../../icons";
export default function Mapping() {
return (
<>
<PageMeta title="Content Mapping - IGNY8" description="Keyword to content mapping" />
<PageHeader
title="Content Mapping"
badge={{ icon: <PieChartIcon />, color: 'indigo' }}
/>
<ComponentCard title="Coming Soon" desc="Keyword to content mapping">
<div className="text-center py-8">
<p className="text-gray-600 dark:text-gray-400">
Content Mapping - Coming Soon
</p>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">
Map keywords and clusters to existing pages and content
</p>
</div>
</ComponentCard>
</>
);
}

View File

@@ -382,29 +382,29 @@ export default function ContentCalendar() {
if (!activeSite) {
return (
<div className="p-6">
<>
<PageMeta title="Content Calendar" description="View and manage scheduled content" />
<div className="flex flex-col items-center justify-center h-64 gap-4">
<CalendarIcon className="w-12 h-12 text-gray-400" />
<p className="text-gray-600 dark:text-gray-400">Please select a site from the header to view the content calendar</p>
</div>
</div>
</>
);
}
if (loading) {
return (
<div className="p-6">
<>
<PageMeta title="Content Calendar" description="View and manage scheduled content" />
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading calendar...</div>
</div>
</div>
</>
);
}
return (
<div className="p-6">
<>
<PageMeta title="Content Calendar" description="View and manage scheduled content" />
<PageHeader
@@ -803,6 +803,6 @@ export default function ContentCalendar() {
</ComponentCard>
</div>
</div>
</div>
</>
);
}

View File

@@ -7,6 +7,7 @@ import Badge from '../../components/ui/badge/Badge';
import PageHeader from '../../components/common/PageHeader';
import { PieChartIcon } from '../../icons';
import { Tooltip } from '../../components/ui/tooltip/Tooltip';
import { usePageLoading } from '../../context/PageLoadingContext';
interface IndustryWithData extends Industry {
keywordsCount: number;
@@ -26,8 +27,8 @@ const formatVolume = (volume: number): string => {
export default function Industries() {
const toast = useToast();
const { startLoading, stopLoading } = usePageLoading();
const [industries, setIndustries] = useState<IndustryWithData[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadIndustries();
@@ -35,7 +36,7 @@ export default function Industries() {
const loadIndustries = async () => {
try {
setLoading(true);
startLoading('Loading industries...');
const response = await fetchIndustries();
const industriesList = response.industries || [];
@@ -86,7 +87,7 @@ export default function Industries() {
} catch (error: any) {
toast.error(`Failed to load industries: ${error.message}`);
} finally {
setLoading(false);
stopLoading();
}
};
@@ -98,18 +99,12 @@ export default function Industries() {
badge={{ icon: <PieChartIcon />, color: 'blue' }}
hideSiteSector={true}
/>
<div className="p-6">
<div className="mb-6">
<p className="text-gray-600 dark:text-gray-400">
Explore our comprehensive global database of industries, sectors, and high-volume keywords
</p>
</div>
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading industries...</div>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{industries.map((industry) => (
<Card
@@ -184,8 +179,6 @@ export default function Industries() {
</Card>
))}
</div>
)}
</div>
</>
);
}

View File

@@ -4,12 +4,13 @@ import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchSeedKeywords, SeedKeyword, fetchIndustries, Industry } from '../../services/api';
import { Card } from '../../components/ui/card';
import Badge from '../../components/ui/badge/Badge';
import { usePageLoading } from '../../context/PageLoadingContext';
export default function SeedKeywords() {
const toast = useToast();
const { startLoading, stopLoading } = usePageLoading();
const [keywords, setKeywords] = useState<SeedKeyword[]>([]);
const [industries, setIndustries] = useState<Industry[]>([]);
const [loading, setLoading] = useState(true);
const [selectedIndustry, setSelectedIndustry] = useState<number | null>(null);
const [searchTerm, setSearchTerm] = useState('');
@@ -29,7 +30,7 @@ export default function SeedKeywords() {
const loadKeywords = async () => {
try {
setLoading(true);
startLoading('Loading seed keywords...');
const response = await fetchSeedKeywords({
industry: selectedIndustry || undefined,
search: searchTerm || undefined,
@@ -38,12 +39,12 @@ export default function SeedKeywords() {
} catch (error: any) {
toast.error(`Failed to load seed keywords: ${error.message}`);
} finally {
setLoading(false);
stopLoading();
}
};
return (
<div className="p-6">
<>
<PageMeta title="Seed Keywords" />
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Seed Keywords</h1>
@@ -72,11 +73,6 @@ export default function SeedKeywords() {
/>
</div>
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading...</div>
</div>
) : (
<Card className="p-6">
<div className="overflow-x-auto">
<table className="w-full">
@@ -118,7 +114,7 @@ export default function SeedKeywords() {
</div>
</Card>
)}
</div>
</>
);
}

View File

@@ -1,22 +0,0 @@
import PageMeta from "../components/common/PageMeta";
import ComponentCard from "../components/common/ComponentCard";
export default function Schedules() {
return (
<>
<PageMeta title="Schedules - IGNY8" description="Automation schedules" />
<ComponentCard title="Coming Soon" desc="Automation schedules">
<div className="text-center py-8">
<p className="text-gray-600 dark:text-gray-400">
Schedules - Coming Soon
</p>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">
Content scheduling and automation for consistent publishing
</p>
</div>
</ComponentCard>
</>
);
}

View File

@@ -3,11 +3,12 @@ import PageMeta from '../../components/common/PageMeta';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchAPI } from '../../services/api';
import { Card } from '../../components/ui/card';
import { usePageLoading } from '../../context/PageLoadingContext';
export default function AccountSettings() {
const toast = useToast();
const [settings, setSettings] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const { startLoading, stopLoading } = usePageLoading();
useEffect(() => {
loadSettings();
@@ -15,34 +16,28 @@ export default function AccountSettings() {
const loadSettings = async () => {
try {
setLoading(true);
startLoading('Loading account settings...');
const response = await fetchAPI('/v1/system/settings/account/');
setSettings(response.results || []);
} catch (error: any) {
toast.error(`Failed to load account settings: ${error.message}`);
} finally {
setLoading(false);
stopLoading();
}
};
return (
<div className="p-6">
<>
<PageMeta title="Account Settings" />
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Account Settings</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">Manage your account preferences and profile</p>
</div>
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading...</div>
</div>
) : (
<Card className="p-6">
<p className="text-gray-600 dark:text-gray-400">Account settings management interface coming soon.</p>
</Card>
)}
</div>
</>
);
}

View File

@@ -66,18 +66,18 @@ const CreditsAndBilling: React.FC = () => {
if (loading) {
return (
<div className="p-6">
<>
<PageMeta title="Usage & Billing" description="View your usage and billing" />
<div className="text-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600 mx-auto"></div>
<p className="mt-4 text-gray-600 dark:text-gray-400">Loading billing data...</p>
</div>
</div>
</>
);
}
return (
<div className="p-6">
<>
<PageMeta title="Usage & Billing" description="View your usage and billing" />
<div className="flex items-center justify-between mb-6">
@@ -243,7 +243,7 @@ const CreditsAndBilling: React.FC = () => {
</div>
</ComponentCard>
)}
</div>
</>
);
};

View File

@@ -1,69 +0,0 @@
import PageMeta from "../../components/common/PageMeta";
import ComponentCard from "../../components/common/ComponentCard";
import { Card } from "../../components/ui/card";
import { DownloadIcon, UploadIcon, DatabaseIcon, FileArchiveIcon, CheckCircleIcon } from '../../icons';
export default function ImportExport() {
return (
<>
<PageMeta title="Import/Export - IGNY8" description="Data management" />
<Card className="p-8">
<div className="text-center py-8 max-w-3xl mx-auto">
<div className="mb-6 flex justify-center">
<div className="p-4 bg-brand-100 dark:bg-brand-900/30 rounded-full">
<DatabaseIcon className="w-12 h-12 text-brand-600 dark:text-brand-400" />
</div>
</div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-3">
Coming Soon: Manage Your Data
</h1>
<p className="text-lg text-gray-600 dark:text-gray-400 mb-8">
Import and Export Your Content - Backup your keywords, articles, and settings. Move your content to other platforms. Download everything safely.
</p>
<div className="bg-gradient-to-br from-brand-50 to-purple-50 dark:from-brand-900/20 dark:to-purple-900/20 rounded-lg p-6 border border-brand-200 dark:border-brand-800">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
What will be available:
</h2>
<div className="space-y-3 text-left">
<div className="flex items-start gap-3">
<CheckCircleIcon className="w-5 h-5 text-success-600 dark:text-success-400 flex-shrink-0 mt-0.5" />
<div className="text-gray-700 dark:text-gray-300">
<strong>Export your keywords as a file</strong> (backup or share)
</div>
</div>
<div className="flex items-start gap-3">
<CheckCircleIcon className="w-5 h-5 text-success-600 dark:text-success-400 flex-shrink-0 mt-0.5" />
<div className="text-gray-700 dark:text-gray-300">
<strong>Export all your articles</strong> in different formats
</div>
</div>
<div className="flex items-start gap-3">
<CheckCircleIcon className="w-5 h-5 text-success-600 dark:text-success-400 flex-shrink-0 mt-0.5" />
<div className="text-gray-700 dark:text-gray-300">
<strong>Import keywords from other sources</strong>
</div>
</div>
<div className="flex items-start gap-3">
<CheckCircleIcon className="w-5 h-5 text-success-600 dark:text-success-400 flex-shrink-0 mt-0.5" />
<div className="text-gray-700 dark:text-gray-300">
<strong>Backup and restore</strong> your entire account
</div>
</div>
<div className="flex items-start gap-3">
<CheckCircleIcon className="w-5 h-5 text-success-600 dark:text-success-400 flex-shrink-0 mt-0.5" />
<div className="text-gray-700 dark:text-gray-300">
<strong>Download your settings</strong> and configurations
</div>
</div>
</div>
</div>
</div>
</Card>
</>
);
}

View File

@@ -1,14 +1,15 @@
import { useState, useEffect } from 'react';
import PageMeta from '../../components/common/PageMeta';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { usePageLoading } from '../../context/PageLoadingContext';
import { fetchIndustries, Industry } from '../../services/api';
import { Card } from '../../components/ui/card';
import Badge from '../../components/ui/badge/Badge';
export default function Industries() {
const toast = useToast();
const { startLoading, stopLoading } = usePageLoading();
const [industries, setIndustries] = useState<Industry[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadIndustries();
@@ -16,29 +17,24 @@ export default function Industries() {
const loadIndustries = async () => {
try {
setLoading(true);
startLoading('Loading industries...');
const response = await fetchIndustries();
setIndustries(response.industries || []);
} catch (error: any) {
toast.error(`Failed to load industries: ${error.message}`);
} finally {
setLoading(false);
stopLoading();
}
};
return (
<div className="p-6">
<>
<PageMeta title="Industries" />
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Industries</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">Manage global industry templates (Admin Only)</p>
</div>
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading...</div>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{industries.map((industry) => (
<Card key={industry.id} className="p-6">
@@ -57,8 +53,7 @@ export default function Industries() {
</Card>
))}
</div>
)}
</div>
</>
);
}

View File

@@ -4,6 +4,7 @@ import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchAPI } from '../../services/api';
import { PricingPlan } from '../../components/ui/pricing-table';
import PricingTable1 from '../../components/ui/pricing-table/pricing-table-1';
import { usePageLoading } from '../../context/PageLoadingContext';
interface Plan {
id: number;
@@ -110,8 +111,8 @@ const getPlanDescription = (plan: Plan): string => {
export default function Plans() {
const toast = useToast();
const { startLoading, stopLoading } = usePageLoading();
const [plans, setPlans] = useState<Plan[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadPlans();
@@ -119,7 +120,7 @@ export default function Plans() {
const loadPlans = async () => {
try {
setLoading(true);
startLoading('Loading plans...');
const response: PlanResponse = await fetchAPI('/v1/auth/plans/');
// Filter only active plans and sort by price
const activePlans = (response.results || [])
@@ -133,7 +134,7 @@ export default function Plans() {
} catch (error: any) {
toast.error(`Failed to load plans: ${error.message}`);
} finally {
setLoading(false);
stopLoading();
}
};
@@ -148,7 +149,7 @@ export default function Plans() {
);
return (
<div className="p-6">
<>
<PageMeta title="Plans" />
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Plans</h1>
@@ -157,11 +158,7 @@ export default function Plans() {
</p>
</div>
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading plans...</div>
</div>
) : pricingPlans.length === 0 ? (
{pricingPlans.length === 0 ? (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">No active plans available</div>
</div>
@@ -183,6 +180,6 @@ export default function Plans() {
</div>
</>
)}
</div>
</>
);
}

View File

@@ -4,6 +4,7 @@
*/
import React, { useState, useEffect } from 'react';
import PageMeta from '../../components/common/PageMeta';
import { usePageLoading } from '../../context/PageLoadingContext';
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
import Checkbox from '../../components/form/input/Checkbox';
@@ -16,7 +17,7 @@ import { fetchAPI } from '../../services/api';
export default function Publishing() {
const toast = useToast();
const [loading, setLoading] = useState(true);
const { startLoading, stopLoading } = usePageLoading();
const [saving, setSaving] = useState(false);
const [defaultDestinations, setDefaultDestinations] = useState<string[]>(['sites']);
const [autoPublishEnabled, setAutoPublishEnabled] = useState(false);
@@ -31,7 +32,7 @@ export default function Publishing() {
const loadSettings = async () => {
try {
setLoading(true);
startLoading('Loading publishing settings...');
// TODO: Load from backend API when endpoint is available
// For now, use defaults
setDefaultDestinations(['sites']);
@@ -43,7 +44,7 @@ export default function Publishing() {
} catch (error: any) {
toast.error(`Failed to load settings: ${error.message}`);
} finally {
setLoading(false);
stopLoading();
}
};
@@ -73,19 +74,8 @@ export default function Publishing() {
{ value: 'shopify', label: 'Publish to Shopify (your Shopify store)' },
];
if (loading) {
return (
<div className="p-6">
<PageMeta title="Publishing Settings" />
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading...</div>
</div>
</div>
);
}
return (
<div className="p-6">
<>
<PageMeta title="Publishing Settings - IGNY8" />
<div className="mb-6">
@@ -230,7 +220,7 @@ export default function Publishing() {
</Button>
</div>
</div>
</div>
</>
);
}

View File

@@ -1,325 +0,0 @@
import { useState, useEffect } from "react";
import PageMeta from "../../components/common/PageMeta";
import ComponentCard from "../../components/common/ComponentCard";
import { fetchAPI } from "../../services/api";
interface SystemStatus {
timestamp: string;
system: {
cpu: { usage_percent: number; cores: number; status: string };
memory: { total_gb: number; used_gb: number; available_gb: number; usage_percent: number; status: string };
disk: { total_gb: number; used_gb: number; free_gb: number; usage_percent: number; status: string };
};
database: {
connected: boolean;
version: string;
size: string;
active_connections: number;
status: string;
};
redis: {
connected: boolean;
status: string;
};
celery: {
workers: string[];
worker_count: number;
tasks: { active: number; scheduled: number; reserved: number };
status: string;
};
processes: {
by_stack: {
[key: string]: { count: number; cpu: number; memory_mb: number };
};
};
modules: {
planner: { keywords: number; clusters: number; content_ideas: number };
writer: { tasks: number; images: number };
};
}
const getStatusColor = (status: string) => {
switch (status) {
case 'healthy': return 'text-success-600 dark:text-success-400';
case 'warning': return 'text-warning-600 dark:text-warning-400';
case 'critical': return 'text-error-600 dark:text-error-400';
default: return 'text-gray-600 dark:text-gray-400';
}
};
const getStatusBadge = (status: string) => {
switch (status) {
case 'healthy': return 'bg-success-100 text-success-800 dark:bg-success-900/30 dark:text-success-400';
case 'warning': return 'bg-warning-100 text-warning-800 dark:bg-warning-900/30 dark:text-warning-400';
case 'critical': return 'bg-error-100 text-error-800 dark:bg-error-900/30 dark:text-error-400';
default: return 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400';
}
};
export default function Status() {
const [status, setStatus] = useState<SystemStatus | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchStatus = async () => {
try {
// fetchAPI extracts data from unified format {success: true, data: {...}}
// So response IS the data object
const response = await fetchAPI('/v1/system/status/');
setStatus(response);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchStatus();
const interval = setInterval(fetchStatus, 30000); // Refresh every 30 seconds
return () => clearInterval(interval);
}, []);
if (loading) {
return (
<>
<PageMeta title="System Status - IGNY8" description="System monitoring" />
<ComponentCard title="System Status" desc="Loading system information...">
<div className="text-center py-8">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900 dark:border-white mx-auto"></div>
</div>
</ComponentCard>
</>
);
}
if (error || !status) {
return (
<>
<PageMeta title="System Status - IGNY8" description="System monitoring" />
<ComponentCard title="System Status" desc="Error loading system information">
<div className="text-center py-8 text-error-600 dark:text-error-400">
{error || 'Failed to load system status'}
</div>
</ComponentCard>
</>
);
}
return (
<>
<PageMeta title="System Status - IGNY8" description="System monitoring" />
<div className="space-y-6">
{/* System Resources */}
<ComponentCard title="System Resources" desc="CPU, Memory, and Disk Usage">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* CPU */}
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">CPU</span>
<span className={`text-xs px-2 py-1 rounded ${getStatusBadge(status.system?.cpu?.status || 'unknown')}`}>
{status.system?.cpu?.status || 'unknown'}
</span>
</div>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-4">
<div
className={`h-4 rounded-full ${
(status.system?.cpu?.usage_percent || 0) < 80 ? 'bg-success-500' :
(status.system?.cpu?.usage_percent || 0) < 95 ? 'bg-warning-500' : 'bg-error-500'
}`}
style={{ width: `${status.system?.cpu?.usage_percent || 0}%` }}
></div>
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
{status.system?.cpu?.usage_percent?.toFixed(1)}% used ({status.system?.cpu?.cores} cores)
</div>
</div>
{/* Memory */}
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Memory</span>
<span className={`text-xs px-2 py-1 rounded ${getStatusBadge(status.system?.memory?.status || 'unknown')}`}>
{status.system?.memory?.status || 'unknown'}
</span>
</div>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-4">
<div
className={`h-4 rounded-full ${
(status.system?.memory?.usage_percent || 0) < 80 ? 'bg-success-500' :
(status.system?.memory?.usage_percent || 0) < 95 ? 'bg-warning-500' : 'bg-error-500'
}`}
style={{ width: `${status.system?.memory?.usage_percent || 0}%` }}
></div>
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
{status.system?.memory?.used_gb?.toFixed(1)} GB / {status.system?.memory?.total_gb?.toFixed(1)} GB
</div>
</div>
{/* Disk */}
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Disk</span>
<span className={`text-xs px-2 py-1 rounded ${getStatusBadge(status.system?.disk?.status || 'unknown')}`}>
{status.system?.disk?.status || 'unknown'}
</span>
</div>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-4">
<div
className={`h-4 rounded-full ${
(status.system?.disk?.usage_percent || 0) < 80 ? 'bg-success-500' :
(status.system?.disk?.usage_percent || 0) < 95 ? 'bg-warning-500' : 'bg-error-500'
}`}
style={{ width: `${status.system?.disk?.usage_percent || 0}%` }}
></div>
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
{status.system?.disk?.used_gb?.toFixed(1)} GB / {status.system?.disk?.total_gb?.toFixed(1)} GB
</div>
</div>
</div>
</ComponentCard>
{/* Services Status */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Database */}
<ComponentCard title="Database" desc="PostgreSQL Status">
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600 dark:text-gray-400">Status</span>
<span className={`text-xs px-2 py-1 rounded ${getStatusBadge(status.database?.status || 'unknown')}`}>
{status.database?.connected ? 'Connected' : 'Disconnected'}
</span>
</div>
{status.database?.version && (
<div className="text-sm">
<span className="text-gray-600 dark:text-gray-400">Version:</span>
<span className="ml-2 text-gray-800 dark:text-gray-200">{status.database.version.split(',')[0]}</span>
</div>
)}
{status.database?.size && (
<div className="text-sm">
<span className="text-gray-600 dark:text-gray-400">Size:</span>
<span className="ml-2 text-gray-800 dark:text-gray-200">{status.database.size}</span>
</div>
)}
<div className="text-sm">
<span className="text-gray-600 dark:text-gray-400">Active Connections:</span>
<span className="ml-2 text-gray-800 dark:text-gray-200">{status.database?.active_connections || 0}</span>
</div>
</div>
</ComponentCard>
{/* Redis */}
<ComponentCard title="Redis" desc="Cache & Message Broker">
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600 dark:text-gray-400">Status</span>
<span className={`text-xs px-2 py-1 rounded ${getStatusBadge(status.redis?.status || 'unknown')}`}>
{status.redis?.connected ? 'Connected' : 'Disconnected'}
</span>
</div>
</div>
</ComponentCard>
{/* Celery */}
<ComponentCard title="Celery" desc="Task Queue Workers">
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600 dark:text-gray-400">Workers</span>
<span className={`text-xs px-2 py-1 rounded ${getStatusBadge(status.celery?.status || 'unknown')}`}>
{status.celery?.worker_count || 0} active
</span>
</div>
<div className="text-sm">
<span className="text-gray-600 dark:text-gray-400">Active Tasks:</span>
<span className="ml-2 text-gray-800 dark:text-gray-200">{status.celery?.tasks?.active || 0}</span>
</div>
<div className="text-sm">
<span className="text-gray-600 dark:text-gray-400">Scheduled:</span>
<span className="ml-2 text-gray-800 dark:text-gray-200">{status.celery?.tasks?.scheduled || 0}</span>
</div>
<div className="text-sm">
<span className="text-gray-600 dark:text-gray-400">Reserved:</span>
<span className="ml-2 text-gray-800 dark:text-gray-200">{status.celery?.tasks?.reserved || 0}</span>
</div>
</div>
</ComponentCard>
</div>
{/* Process Monitoring by Stack */}
<ComponentCard title="Process Monitoring" desc="Resource usage by technology stack">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead>
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Stack</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Processes</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">CPU %</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Memory (MB)</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
{Object.entries(status.processes?.by_stack || {}).map(([stack, stats]) => (
<tr key={stack}>
<td className="px-4 py-3 text-sm font-medium text-gray-800 dark:text-gray-200 capitalize">{stack}</td>
<td className="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">{stats.count}</td>
<td className="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">{stats.cpu.toFixed(2)}%</td>
<td className="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">{stats.memory_mb.toFixed(2)}</td>
</tr>
))}
</tbody>
</table>
</div>
</ComponentCard>
{/* Module Statistics */}
<ComponentCard title="Module Statistics" desc="Data counts by module">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Planner Module */}
<div className="space-y-3">
<h4 className="text-sm font-semibold text-gray-800 dark:text-gray-200">Planner Module</h4>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-600 dark:text-gray-400">Keywords:</span>
<span className="text-gray-800 dark:text-gray-200">{status.modules?.planner?.keywords?.toLocaleString() || 0}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600 dark:text-gray-400">Clusters:</span>
<span className="text-gray-800 dark:text-gray-200">{status.modules?.planner?.clusters?.toLocaleString() || 0}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600 dark:text-gray-400">Content Ideas:</span>
<span className="text-gray-800 dark:text-gray-200">{status.modules?.planner?.content_ideas?.toLocaleString() || 0}</span>
</div>
</div>
</div>
{/* Writer Module */}
<div className="space-y-3">
<h4 className="text-sm font-semibold text-gray-800 dark:text-gray-200">Writer Module</h4>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-600 dark:text-gray-400">Tasks:</span>
<span className="text-gray-800 dark:text-gray-200">{status.modules?.writer?.tasks?.toLocaleString() || 0}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600 dark:text-gray-400">Images:</span>
<span className="text-gray-800 dark:text-gray-200">{status.modules?.writer?.images?.toLocaleString() || 0}</span>
</div>
</div>
</div>
</div>
</ComponentCard>
{/* Last Updated */}
<div className="text-center text-sm text-gray-500 dark:text-gray-400">
Last updated: {new Date(status.timestamp).toLocaleString()}
</div>
</div>
</>
);
}

View File

@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react';
import PageMeta from '../../components/common/PageMeta';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { usePageLoading } from '../../context/PageLoadingContext';
import { fetchAPI } from '../../services/api';
import { Card } from '../../components/ui/card';
import Badge from '../../components/ui/badge/Badge';
@@ -15,8 +16,8 @@ interface Subscription {
export default function Subscriptions() {
const toast = useToast();
const { startLoading, stopLoading } = usePageLoading();
const [subscriptions, setSubscriptions] = useState<Subscription[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadSubscriptions();
@@ -24,13 +25,13 @@ export default function Subscriptions() {
const loadSubscriptions = async () => {
try {
setLoading(true);
startLoading('Loading subscriptions...');
const response = await fetchAPI('/v1/auth/subscriptions/');
setSubscriptions(response.results || []);
} catch (error: any) {
toast.error(`Failed to load subscriptions: ${error.message}`);
} finally {
setLoading(false);
stopLoading();
}
};
@@ -48,18 +49,13 @@ export default function Subscriptions() {
};
return (
<div className="p-6">
<>
<PageMeta title="Subscriptions" />
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Subscriptions</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">Manage account subscriptions</p>
</div>
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading...</div>
</div>
) : (
<Card className="p-6">
<div className="overflow-x-auto">
<table className="w-full">
@@ -92,8 +88,7 @@ export default function Subscriptions() {
</table>
</div>
</Card>
)}
</div>
</>
);
}

View File

@@ -3,11 +3,12 @@ import PageMeta from '../../components/common/PageMeta';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchAPI } from '../../services/api';
import { Card } from '../../components/ui/card';
import { usePageLoading } from '../../context/PageLoadingContext';
export default function SystemSettings() {
const toast = useToast();
const [settings, setSettings] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const { startLoading, stopLoading } = usePageLoading();
useEffect(() => {
loadSettings();
@@ -15,34 +16,28 @@ export default function SystemSettings() {
const loadSettings = async () => {
try {
setLoading(true);
startLoading('Loading system settings...');
const response = await fetchAPI('/v1/system/settings/system/');
setSettings(response.results || []);
} catch (error: any) {
toast.error(`Failed to load system settings: ${error.message}`);
} finally {
setLoading(false);
stopLoading();
}
};
return (
<div className="p-6">
<>
<PageMeta title="System Settings" />
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">System Settings</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">Global platform-wide settings</p>
</div>
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading...</div>
</div>
) : (
<Card className="p-6">
<p className="text-gray-600 dark:text-gray-400">System settings management interface coming soon.</p>
</Card>
)}
</div>
</>
);
}

View File

@@ -1,6 +1,7 @@
import { useState, useEffect } from 'react';
import PageMeta from '../../components/common/PageMeta';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { usePageLoading } from '../../context/PageLoadingContext';
import { fetchAPI } from '../../services/api';
import { Card } from '../../components/ui/card';
import Badge from '../../components/ui/badge/Badge';
@@ -15,8 +16,8 @@ interface User {
export default function Users() {
const toast = useToast();
const { startLoading, stopLoading } = usePageLoading();
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadUsers();
@@ -24,29 +25,24 @@ export default function Users() {
const loadUsers = async () => {
try {
setLoading(true);
startLoading('Loading users...');
const response = await fetchAPI('/v1/auth/users/');
setUsers(response.results || []);
} catch (error: any) {
toast.error(`Failed to load users: ${error.message}`);
} finally {
setLoading(false);
stopLoading();
}
};
return (
<div className="p-6">
<>
<PageMeta title="Users" />
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Users</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">Manage account users and permissions</p>
</div>
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading...</div>
</div>
) : (
<Card className="p-6">
<div className="overflow-x-auto">
<table className="w-full">
@@ -77,8 +73,7 @@ export default function Users() {
</table>
</div>
</Card>
)}
</div>
</>
);
}

View File

@@ -10,6 +10,7 @@ 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';
import { usePageLoading } from '../../context/PageLoadingContext';
import WorkflowGuide from '../../components/onboarding/WorkflowGuide';
import {
fetchSeedKeywords,
@@ -34,6 +35,7 @@ import Label from '../../components/form/Label';
export default function IndustriesSectorsKeywords() {
const toast = useToast();
const { startLoading, stopLoading } = usePageLoading();
const { activeSite } = useSiteStore();
const { activeSector, loadSectorsForSite } = useSectorStore();
const { pageSize } = usePageSizeStore();
@@ -42,7 +44,6 @@ export default function IndustriesSectorsKeywords() {
// Data state
const [sites, setSites] = useState<Site[]>([]);
const [seedKeywords, setSeedKeywords] = useState<(SeedKeyword & { isAdded?: boolean })[]>([]);
const [loading, setLoading] = useState(true);
const [showContent, setShowContent] = useState(false);
const [selectedIds, setSelectedIds] = useState<string[]>([]);
// Track recently added keywords to preserve their state during reload
@@ -82,14 +83,14 @@ export default function IndustriesSectorsKeywords() {
const loadInitialData = async () => {
try {
setLoading(true);
startLoading('Loading sites...');
const response = await fetchSites();
const activeSites = (response.results || []).filter(site => site.is_active);
setSites(activeSites);
} catch (error: any) {
toast.error(`Failed to load sites: ${error.message}`);
} finally {
setLoading(false);
stopLoading();
}
};
@@ -640,24 +641,6 @@ export default function IndustriesSectorsKeywords() {
};
}, [activeSector, handleAddToWorkflow]);
// Show loading state
if (loading) {
return (
<>
<PageMeta title="Add Keywords" description="Browse and add keywords to your workflow" />
<PageHeader
title="Add Keywords"
badge={{ icon: <BoltIcon />, color: 'blue' }}
/>
<div className="p-6">
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading...</div>
</div>
</div>
</>
);
}
// Show WorkflowGuide if no sites
if (sites.length === 0) {
return (

View File

@@ -150,7 +150,7 @@ export default function SiteContentManager() {
];
return (
<div className="p-6">
<>
<PageMeta title="Site Content Manager - IGNY8" />
<PageHeader
@@ -309,7 +309,7 @@ export default function SiteContentManager() {
)}
</>
)}
</div>
</>
);
}

View File

@@ -231,18 +231,18 @@ export default function SiteDashboard() {
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>
@@ -250,12 +250,12 @@ export default function SiteDashboard() {
Back to Sites
</Button>
</Card>
</div>
</>
);
}
return (
<div className="p-6">
<>
<PageMeta title={`${site.name} - Dashboard`} />
<PageHeader
title="Site Dashboard"
@@ -384,7 +384,7 @@ export default function SiteDashboard() {
No recent activity
</p>
</Card>
</div>
</>
);
}

View File

@@ -1,43 +0,0 @@
/**
* Site Editor - DEPRECATED
*
* Legacy SiteBlueprint page editor has been removed.
* Use Writer module for content creation and editing.
*/
import React from 'react';
import { useNavigate } from 'react-router-dom';
import PageMeta from '../../components/common/PageMeta';
import PageHeader from '../../components/common/PageHeader';
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
import { AlertIcon } from '../../icons';
export default function Editor() {
const navigate = useNavigate();
return (
<div className="space-y-6">
<PageMeta
title="Site Editor"
description="Legacy site editor features"
/>
<PageHeader
title="Site Editor"
subtitle="This feature has been deprecated"
backLink="../"
/>
<Card className="p-8 text-center">
<AlertIcon className="w-16 h-16 text-warning-500 mx-auto mb-4" />
<h2 className="text-2xl font-bold mb-2">Feature Deprecated</h2>
<p className="text-gray-600 dark:text-gray-400 mb-6">
The SiteBlueprint page editor has been removed.
Please use the Writer module to create and edit content.
</p>
<Button onClick={() => navigate('../')} variant="primary">
Return to Sites
</Button>
</Card>
</div>
);
}

View File

@@ -479,17 +479,17 @@ export default function SiteList() {
if (loading) {
return (
<div className="p-6">
<>
<PageMeta title="Site List" />
<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="Your Websites - IGNY8" />
<PageHeader
title="Your Websites"
@@ -693,6 +693,6 @@ export default function SiteList() {
)}
</>
)}
</div>
</>
);
}

View File

@@ -266,17 +266,17 @@ export default function PageManager() {
if (loading) {
return (
<div className="p-6">
<>
<PageMeta title="Page Manager" />
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading pages...</div>
</div>
</div>
</>
);
}
return (
<div className="p-6">
<>
<PageMeta title="Page Manager - IGNY8" />
<PageHeader
@@ -400,7 +400,7 @@ export default function PageManager() {
</Card>
</>
)}
</div>
</>
);
}

View File

@@ -218,17 +218,17 @@ export default function PostEditor() {
if (loading) {
return (
<div className="p-6">
<>
<PageMeta title="Post Editor" />
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading post...</div>
</div>
</div>
</>
);
}
return (
<div className="p-6">
<>
<PageMeta title={content.id ? 'Edit Post' : 'New Post'} />
<div className="flex gap-6">
@@ -677,7 +677,7 @@ export default function PostEditor() {
</div>
)}
</div>
</div>
</>
);
}

View File

@@ -1,452 +0,0 @@
/**
* Publishing Queue Page
* Shows scheduled content for publishing to external site
* Allows reordering, pausing, and viewing calendar
*/
import React, { useState, useEffect, useCallback } 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 { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
import IconButton from '../../components/ui/button/IconButton';
import { ButtonGroup, ButtonGroupItem } from '../../components/ui/button-group/ButtonGroup';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchContent, Content } from '../../services/api';
import {
ClockIcon,
CheckCircleIcon,
ArrowRightIcon,
CalendarIcon,
ListIcon,
PauseIcon,
PlayIcon,
TrashBinIcon,
EyeIcon,
} from '../../icons';
type ViewMode = 'list' | 'calendar';
interface QueueItem extends Content {
isPaused?: boolean;
}
export default function PublishingQueue() {
const { id: siteId } = useParams<{ id: string }>();
const navigate = useNavigate();
const toast = useToast();
const [loading, setLoading] = useState(true);
const [queueItems, setQueueItems] = useState<QueueItem[]>([]);
const [viewMode, setViewMode] = useState<ViewMode>('list');
const [draggedItem, setDraggedItem] = useState<QueueItem | null>(null);
const [stats, setStats] = useState({
scheduled: 0,
publishing: 0,
published: 0,
failed: 0,
});
const loadQueue = useCallback(async () => {
try {
setLoading(true);
// Fetch content that is scheduled or publishing
const response = await fetchContent({
site_id: Number(siteId),
page_size: 100,
});
const items = (response.results || []).filter(
(c: Content) => c.site_status === 'scheduled' || c.site_status === 'publishing'
);
// Sort by scheduled_publish_at
items.sort((a: Content, b: Content) => {
const dateA = a.scheduled_publish_at ? new Date(a.scheduled_publish_at).getTime() : 0;
const dateB = b.scheduled_publish_at ? new Date(b.scheduled_publish_at).getTime() : 0;
return dateA - dateB;
});
setQueueItems(items);
// Calculate stats
const allContent = response.results || [];
setStats({
scheduled: allContent.filter((c: Content) => c.site_status === 'scheduled').length,
publishing: allContent.filter((c: Content) => c.site_status === 'publishing').length,
published: allContent.filter((c: Content) => c.site_status === 'published').length,
failed: allContent.filter((c: Content) => c.site_status === 'failed').length,
});
} catch (error: any) {
toast.error(`Failed to load queue: ${error.message}`);
} finally {
setLoading(false);
}
}, [siteId, toast]);
useEffect(() => {
if (siteId) {
loadQueue();
}
}, [siteId, loadQueue]);
// Drag and drop handlers
const handleDragStart = (e: React.DragEvent, item: QueueItem) => {
setDraggedItem(item);
e.dataTransfer.effectAllowed = 'move';
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
};
const handleDrop = (e: React.DragEvent, targetItem: QueueItem) => {
e.preventDefault();
if (!draggedItem || draggedItem.id === targetItem.id) return;
const newItems = [...queueItems];
const draggedIndex = newItems.findIndex(item => item.id === draggedItem.id);
const targetIndex = newItems.findIndex(item => item.id === targetItem.id);
// Remove dragged item and insert at target position
newItems.splice(draggedIndex, 1);
newItems.splice(targetIndex, 0, draggedItem);
setQueueItems(newItems);
setDraggedItem(null);
// TODO: Call API to update scheduled_publish_at based on new order
toast.success('Queue order updated');
};
const handleDragEnd = () => {
setDraggedItem(null);
};
const handlePauseItem = (item: QueueItem) => {
// Toggle pause state (in real implementation, this would call an API)
setQueueItems(prev =>
prev.map(i => i.id === item.id ? { ...i, isPaused: !i.isPaused } : i)
);
toast.info(item.isPaused ? 'Item resumed' : 'Item paused');
};
const handleRemoveFromQueue = (item: QueueItem) => {
// TODO: Call API to set site_status back to 'not_published'
setQueueItems(prev => prev.filter(i => i.id !== item.id));
toast.success('Removed from queue');
};
const handleViewContent = (item: QueueItem) => {
navigate(`/sites/${siteId}/posts/${item.id}`);
};
const formatScheduledTime = (dateStr: string | null | undefined) => {
if (!dateStr) return 'Not scheduled';
const date = new Date(dateStr);
return date.toLocaleString('en-US', {
weekday: 'short',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: true,
});
};
const getStatusBadge = (item: QueueItem) => {
if (item.isPaused) {
return (
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300">
<PauseIcon className="w-3 h-3" />
Paused
</span>
);
}
if (item.site_status === 'publishing') {
return (
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-brand-100 text-brand-700 dark:bg-brand-900 dark:text-brand-300">
<ArrowRightIcon className="w-3 h-3 animate-pulse" />
Publishing...
</span>
);
}
return (
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-warning-100 text-warning-700 dark:bg-warning-900 dark:text-warning-300">
<ClockIcon className="w-3 h-3" />
Scheduled
</span>
);
};
// Calendar view helpers
const getCalendarDays = () => {
const today = new Date();
const days = [];
for (let i = 0; i < 14; i++) {
const date = new Date(today);
date.setDate(today.getDate() + i);
days.push(date);
}
return days;
};
const getItemsForDate = (date: Date) => {
return queueItems.filter(item => {
if (!item.scheduled_publish_at) return false;
const itemDate = new Date(item.scheduled_publish_at);
return (
itemDate.getDate() === date.getDate() &&
itemDate.getMonth() === date.getMonth() &&
itemDate.getFullYear() === date.getFullYear()
);
});
};
if (loading) {
return (
<div className="p-6">
<PageMeta title="Publishing Queue" description="View and manage scheduled content" />
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading queue...</div>
</div>
</div>
);
}
return (
<div className="p-6">
<PageMeta title="Publishing Queue" description="View and manage scheduled content" />
<PageHeader
title="Publishing Queue"
badge={{ icon: <ClockIcon />, color: 'amber' }}
breadcrumb="Sites / Publishing Queue"
/>
{/* Stats Overview */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="size-10 rounded-lg bg-warning-100 dark:bg-warning-900/30 flex items-center justify-center">
<ClockIcon className="w-5 h-5 text-warning-600" />
</div>
<div>
<p className="text-2xl font-bold text-gray-900 dark:text-white">{stats.scheduled}</p>
<p className="text-sm text-gray-600 dark:text-gray-400">Scheduled</p>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="size-10 rounded-lg bg-brand-100 dark:bg-brand-900/30 flex items-center justify-center">
<ArrowRightIcon className="w-5 h-5 text-brand-600" />
</div>
<div>
<p className="text-2xl font-bold text-gray-900 dark:text-white">{stats.publishing}</p>
<p className="text-sm text-gray-600 dark:text-gray-400">Publishing</p>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="size-10 rounded-lg bg-success-100 dark:bg-success-900/30 flex items-center justify-center">
<CheckCircleIcon className="w-5 h-5 text-success-600" />
</div>
<div>
<p className="text-2xl font-bold text-gray-900 dark:text-white">{stats.published}</p>
<p className="text-sm text-gray-600 dark:text-gray-400">Published</p>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="size-10 rounded-lg bg-error-100 dark:bg-error-900/30 flex items-center justify-center">
<TrashBinIcon className="w-5 h-5 text-error-600" />
</div>
<div>
<p className="text-2xl font-bold text-gray-900 dark:text-white">{stats.failed}</p>
<p className="text-sm text-gray-600 dark:text-gray-400">Failed</p>
</div>
</div>
</Card>
</div>
{/* View Toggle */}
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
{queueItems.length} items in queue
</h2>
<ButtonGroup>
<ButtonGroupItem
isActive={viewMode === 'list'}
onClick={() => setViewMode('list')}
startIcon={<ListIcon className="w-4 h-4" />}
>
List
</ButtonGroupItem>
<ButtonGroupItem
isActive={viewMode === 'calendar'}
onClick={() => setViewMode('calendar')}
startIcon={<CalendarIcon className="w-4 h-4" />}
>
Calendar
</ButtonGroupItem>
</ButtonGroup>
</div>
{/* Queue Content */}
{queueItems.length === 0 ? (
<Card className="p-12 text-center">
<ClockIcon className="w-12 h-12 mx-auto text-gray-400 mb-4" />
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
No content scheduled
</h3>
<p className="text-gray-600 dark:text-gray-400 mb-4">
Content will appear here when it's scheduled for publishing.
</p>
<Button variant="outline" onClick={() => navigate(`/sites/${siteId}/settings?tab=publishing`)}>
Configure Publishing Settings
</Button>
</Card>
) : viewMode === 'list' ? (
/* List View */
<ComponentCard title="Queue" desc="Drag items to reorder. Content publishes in order from top to bottom.">
<div className="space-y-2">
{queueItems.map((item, index) => (
<div
key={item.id}
draggable
onDragStart={(e) => handleDragStart(e, item)}
onDragOver={handleDragOver}
onDrop={(e) => handleDrop(e, item)}
onDragEnd={handleDragEnd}
className={`
flex items-center gap-4 p-4 bg-white dark:bg-gray-800 rounded-lg border-2
${draggedItem?.id === item.id ? 'border-brand-500 opacity-50' : 'border-gray-200 dark:border-gray-700'}
${item.isPaused ? 'opacity-60' : ''}
hover:border-brand-300 dark:hover:border-brand-700 transition-all cursor-move
`}
>
{/* Order number */}
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-gray-100 dark:bg-gray-700 flex items-center justify-center text-sm font-medium text-gray-600 dark:text-gray-400">
{index + 1}
</div>
{/* Content info */}
<div className="flex-1 min-w-0">
<h4 className="font-medium text-gray-900 dark:text-white truncate">
{item.title}
</h4>
<div className="flex items-center gap-3 mt-1 text-sm text-gray-500 dark:text-gray-400">
<span className="flex items-center gap-1">
<ClockIcon className="w-3.5 h-3.5" />
{formatScheduledTime(item.scheduled_publish_at)}
</span>
<span className="text-gray-300 dark:text-gray-600">•</span>
<span>{item.content_type}</span>
</div>
</div>
{/* Status badge */}
{getStatusBadge(item)}
{/* Actions */}
<div className="flex items-center gap-1">
<IconButton
icon={<EyeIcon className="w-4 h-4" />}
variant="ghost"
tone="neutral"
size="sm"
onClick={() => handleViewContent(item)}
title="View content"
/>
<IconButton
icon={item.isPaused ? <PlayIcon className="w-4 h-4" /> : <PauseIcon className="w-4 h-4" />}
variant="ghost"
tone="neutral"
size="sm"
onClick={() => handlePauseItem(item)}
title={item.isPaused ? 'Resume' : 'Pause'}
/>
<IconButton
icon={<TrashBinIcon className="w-4 h-4" />}
variant="ghost"
tone="danger"
size="sm"
onClick={() => handleRemoveFromQueue(item)}
title="Remove from queue"
/>
</div>
</div>
))}
</div>
</ComponentCard>
) : (
/* Calendar View */
<ComponentCard title="Calendar View" desc="Content scheduled for the next 14 days">
<div className="grid grid-cols-7 gap-2">
{/* Day headers */}
{['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(day => (
<div key={day} className="text-center text-sm font-medium text-gray-500 dark:text-gray-400 py-2">
{day}
</div>
))}
{/* Calendar days */}
{getCalendarDays().map((date, index) => {
const dayItems = getItemsForDate(date);
const isToday = date.toDateString() === new Date().toDateString();
return (
<div
key={index}
className={`
min-h-[100px] p-2 rounded-lg border
${isToday
? 'border-brand-500 bg-brand-50 dark:bg-brand-900/20'
: 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800'
}
`}
>
<div className={`text-sm font-medium mb-1 ${isToday ? 'text-brand-600' : 'text-gray-600 dark:text-gray-400'}`}>
{date.getDate()}
</div>
<div className="space-y-1">
{dayItems.slice(0, 3).map(item => (
<div
key={item.id}
onClick={() => handleViewContent(item)}
className="text-xs p-1 bg-warning-100 dark:bg-warning-900/30 text-warning-800 dark:text-warning-200 rounded truncate cursor-pointer hover:bg-warning-200 dark:hover:bg-warning-900/50"
title={item.title}
>
{item.title}
</div>
))}
{dayItems.length > 3 && (
<div className="text-xs text-gray-500 dark:text-gray-400">
+{dayItems.length - 3} more
</div>
)}
</div>
</div>
);
})}
</div>
</ComponentCard>
)}
{/* Actions */}
<div className="mt-6 flex justify-between">
<Button variant="outline" onClick={() => navigate(`/sites/${siteId}`)}>
Back to Site
</Button>
<Button variant="primary" onClick={() => navigate(`/sites/${siteId}/settings?tab=publishing`)}>
Publishing Settings
</Button>
</div>
</div>
);
}

View File

@@ -669,17 +669,17 @@ export default function SiteSettings() {
if (loading) {
return (
<div className="p-6">
<>
<PageMeta title="Site Settings" />
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading site settings...</div>
</div>
</div>
</>
);
}
return (
<div className="p-6">
<>
<PageMeta title="Site Settings - IGNY8" />
<PageHeader
title="Site Settings"
@@ -2003,7 +2003,7 @@ export default function SiteSettings() {
</div>
)}
</div>
</>
);
}

View File

@@ -118,18 +118,18 @@ export default function SyncDashboard() {
if (loading) {
return (
<div className="p-6">
<>
<PageMeta title="Sync Dashboard" />
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading sync data...</div>
</div>
</div>
</>
);
}
if (!syncStatus) {
return (
<div className="p-6">
<>
<PageMeta title="Sync Dashboard" />
<Card className="p-6">
<div className="text-center py-8">
@@ -137,7 +137,7 @@ export default function SyncDashboard() {
<p className="text-gray-600 dark:text-gray-400">No sync data available</p>
</div>
</Card>
</div>
</>
);
}
@@ -151,7 +151,7 @@ export default function SyncDashboard() {
(mismatches?.posts.missing_in_igny8.length || 0);
return (
<div className="p-6">
<>
<PageMeta title="Sync Dashboard" />
<PageHeader
@@ -465,7 +465,7 @@ export default function SyncDashboard() {
</div>
)}
</Card>
</div>
</>
);
}

View File

@@ -1,19 +0,0 @@
import ComponentCard from "../../components/common/ComponentCard";
import PageMeta from "../../components/common/PageMeta";
import BasicTableOne from "../../components/tables/BasicTables/BasicTableOne";
export default function BasicTables() {
return (
<>
<PageMeta
title="React.js Basic Tables Dashboard | TailAdmin - Next.js Admin Dashboard Template"
description="This is React.js Basic Tables Dashboard page for TailAdmin - React.js Tailwind CSS Admin Dashboard Template"
/>
<div className="space-y-6">
<ComponentCard title="Basic Table 1">
<BasicTableOne />
</ComponentCard>
</div>
</>
);
}

View File

@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
import PageMeta from '../../components/common/PageMeta';
import PageHeader from '../../components/common/PageHeader';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { usePageLoading } from '../../context/PageLoadingContext';
import { fetchAuthorProfiles, createAuthorProfile, updateAuthorProfile, deleteAuthorProfile, AuthorProfile } from '../../services/api';
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
@@ -11,8 +12,8 @@ import { PlusIcon, UserIcon } from '../../icons';
export default function AuthorProfiles() {
const toast = useToast();
const { startLoading, stopLoading } = usePageLoading();
const [profiles, setProfiles] = useState<AuthorProfile[]>([]);
const [loading, setLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingProfile, setEditingProfile] = useState<AuthorProfile | null>(null);
const [formData, setFormData] = useState({
@@ -29,13 +30,13 @@ export default function AuthorProfiles() {
const loadProfiles = async () => {
try {
setLoading(true);
startLoading('Loading author profiles...');
const response = await fetchAuthorProfiles();
setProfiles(response.results || []);
} catch (error: any) {
toast.error(`Failed to load author profiles: ${error.message}`);
} finally {
setLoading(false);
stopLoading();
}
};
@@ -99,7 +100,7 @@ export default function AuthorProfiles() {
];
return (
<div className="p-6">
<>
<PageMeta title="Writing Styles" />
<PageHeader
title="Writing Styles"
@@ -112,11 +113,6 @@ export default function AuthorProfiles() {
</Button>
</div>
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading...</div>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{profiles.map((profile) => (
<Card key={profile.id} className="p-6">
@@ -148,7 +144,6 @@ export default function AuthorProfiles() {
</Card>
))}
</div>
)}
<FormModal
isOpen={isModalOpen}
@@ -159,7 +154,7 @@ export default function AuthorProfiles() {
data={formData}
onChange={setFormData}
/>
</div>
</>
);
}

View File

@@ -1,397 +0,0 @@
import { useEffect, useState, lazy, Suspense } from "react";
import { Link, useNavigate } from "react-router-dom";
import PageMeta from "../../components/common/PageMeta";
import ComponentCard from "../../components/common/ComponentCard";
import { ProgressBar } from "../../components/ui/progress";
import { ApexOptions } from "apexcharts";
import EnhancedMetricCard from "../../components/dashboard/EnhancedMetricCard";
import PageHeader from "../../components/common/PageHeader";
const Chart = lazy(() => import("react-apexcharts").then((mod) => ({ default: mod.default })));
import {
BoltIcon,
FileTextIcon,
UserIcon,
ShootingStarIcon,
CheckCircleIcon,
ArrowRightIcon,
PlusIcon,
PencilIcon,
ClockIcon,
PieChartIcon,
DocsIcon,
} from "../../icons";
import { useSiteStore } from "../../store/siteStore";
import { useSectorStore } from "../../store/sectorStore";
interface ThinkerStats {
totalPrompts: number;
activeProfiles: number;
strategies: number;
usageThisMonth: number;
}
export default function ThinkerDashboard() {
const navigate = useNavigate();
const { activeSite } = useSiteStore();
const { activeSector } = useSectorStore();
const [stats, setStats] = useState<ThinkerStats | null>(null);
const [loading, setLoading] = useState(true);
const [lastUpdated, setLastUpdated] = useState<Date>(new Date());
// Mock data for now - will be replaced with real API calls
useEffect(() => {
const fetchData = async () => {
setLoading(true);
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 500));
setStats({
totalPrompts: 24,
activeProfiles: 8,
strategies: 12,
usageThisMonth: 342,
});
setLastUpdated(new Date());
setLoading(false);
};
fetchData();
}, [activeSite, activeSector]);
const thinkerModules = [
{
title: "Prompt Library",
description: "Centralized prompt templates and AI instructions",
icon: FileTextIcon,
color: "from-[var(--color-warning)] to-[var(--color-warning-dark)]",
path: "/thinker/prompts",
count: stats?.totalPrompts || 0,
status: "active",
},
{
title: "Author Profiles",
description: "Voice templates and writing style guides",
icon: UserIcon,
color: "from-[var(--color-primary)] to-[var(--color-primary-dark)]",
path: "/thinker/profiles",
count: stats?.activeProfiles || 0,
status: "active",
},
{
title: "Content Strategies",
description: "Brand playbooks and content frameworks",
icon: ShootingStarIcon,
color: "from-[var(--color-purple)] to-[var(--color-purple-dark)]",
path: "/thinker/strategies",
count: stats?.strategies || 0,
status: "active",
},
{
title: "Governance",
description: "Track AI usage, compliance, and version control",
icon: PieChartIcon,
color: "from-[var(--color-success)] to-[var(--color-success-dark)]",
path: "/thinker/governance",
count: 0,
status: "coming-soon",
},
];
const recentPrompts = [
{
id: 1,
name: "Long-form Article Template",
category: "Content Generation",
usage: 45,
lastUsed: "2 hours ago",
},
{
id: 2,
name: "SEO-Optimized Brief",
category: "Content Planning",
usage: 32,
lastUsed: "5 hours ago",
},
{
id: 3,
name: "Brand Voice - Technical",
category: "Author Profile",
usage: 28,
lastUsed: "1 day ago",
},
];
const chartOptions: ApexOptions = {
chart: {
type: "donut",
height: 300,
},
labels: ["Content Generation", "Content Planning", "Image Prompts", "Other"],
colors: ["var(--color-warning)", "var(--color-primary)", "var(--color-purple)", "var(--color-success)"],
legend: {
position: "bottom",
labels: { colors: "var(--color-gray-500)" },
},
dataLabels: {
enabled: true,
formatter: (val: number) => `${val}%`,
},
};
const chartSeries = [35, 28, 22, 15];
return (
<>
<PageMeta title="Strategy Dashboard - IGNY8" description="Manage your content strategy" />
<PageHeader title="Strategy Dashboard" />
<div className="space-y-6">
{/* Key Metrics */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<EnhancedMetricCard
title="Prompt Library"
value={stats?.totalPrompts || 0}
icon={<FileTextIcon className="size-6" />}
trend={0}
accentColor="orange"
/>
<EnhancedMetricCard
title="Author Profiles"
value={stats?.activeProfiles || 0}
icon={<UserIcon className="size-6" />}
trend={0}
accentColor="blue"
/>
<EnhancedMetricCard
title="Strategies"
value={stats?.strategies || 0}
icon={<ShootingStarIcon className="size-6" />}
trend={0}
accentColor="purple"
/>
<EnhancedMetricCard
title="Usage This Month"
value={stats?.usageThisMonth || 0}
icon={<BoltIcon className="size-6" />}
trend={0}
accentColor="green"
/>
</div>
{/* Thinker Modules */}
<ComponentCard title="Thinker Modules" desc="Manage prompts, profiles, and strategies">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{thinkerModules.map((module) => {
const Icon = module.icon;
return (
<Link
key={module.title}
to={module.path}
className="rounded-2xl border-2 border-gray-200 bg-white p-6 hover:shadow-xl hover:-translate-y-1 transition-all group"
>
<div className="flex items-start justify-between mb-4">
<div className={`inline-flex size-14 rounded-xl bg-gradient-to-br ${module.color} items-center justify-center text-white shadow-lg`}>
<Icon className="h-7 w-7" />
</div>
{module.status === "coming-soon" && (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-semibold bg-gray-100 text-gray-600">
Soon
</span>
)}
</div>
<h3 className="text-lg font-bold text-gray-900 mb-2">{module.title}</h3>
<p className="text-sm text-gray-600 mb-4">{module.description}</p>
{module.count > 0 && (
<div className="flex items-center justify-between">
<span className="text-2xl font-bold text-gray-900">{module.count}</span>
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-primary)] group-hover:translate-x-1 transition" />
</div>
)}
{module.status === "coming-soon" && (
<div className="mt-4 pt-4 border-t border-gray-200">
<span className="text-xs text-gray-500">Coming soon</span>
</div>
)}
</Link>
);
})}
</div>
</ComponentCard>
{/* Recent Activity & Usage Chart */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<ComponentCard title="Prompt Usage Distribution" desc="How your prompts are being used">
<Suspense fallback={<div className="h-[300px] flex items-center justify-center">Loading chart...</div>}>
<Chart options={chartOptions} series={chartSeries} type="donut" height={300} />
</Suspense>
</ComponentCard>
<ComponentCard title="Most Used Prompts" desc="Your top-performing prompt templates">
<div className="space-y-4">
{recentPrompts.map((prompt) => (
<div
key={prompt.id}
className="flex items-center gap-4 p-4 rounded-lg border border-gray-200 bg-white hover:shadow-md transition"
>
<div className="size-10 rounded-lg bg-gradient-to-br from-[var(--color-warning)] to-[var(--color-warning-dark)] flex items-center justify-center text-white shadow-md">
<FileTextIcon className="h-5 w-5" />
</div>
<div className="flex-1">
<div className="flex items-center justify-between mb-1">
<h4 className="font-semibold text-gray-900">{prompt.name}</h4>
<span className="text-xs font-semibold text-[var(--color-primary)]">{prompt.usage} uses</span>
</div>
<div className="flex items-center gap-3 text-xs text-gray-600">
<span>{prompt.category}</span>
<span></span>
<span>{prompt.lastUsed}</span>
</div>
</div>
</div>
))}
</div>
</ComponentCard>
</div>
{/* Quick Actions */}
<ComponentCard title="Quick Actions" desc="Create new prompts, profiles, or strategies">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Button
onClick={() => navigate("/thinker/prompts")}
variant="outline"
size="lg"
startIcon={
<div className="size-8 rounded-lg bg-gradient-to-br from-[var(--color-warning)] to-[var(--color-warning-dark)] flex items-center justify-center text-white shadow-md">
<PlusIcon className="h-4 w-4" />
</div>
}
className="!justify-start !h-auto !py-4"
>
<div className="flex-1 text-left">
<h4 className="font-semibold text-gray-900 mb-1">New Prompt</h4>
<p className="text-sm text-gray-600">Create a reusable prompt template</p>
</div>
</Button>
<Button
onClick={() => navigate("/thinker/profiles")}
variant="outline"
size="lg"
startIcon={
<div className="size-8 rounded-lg bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] flex items-center justify-center text-white shadow-md">
<PlusIcon className="h-4 w-4" />
</div>
}
className="!justify-start !h-auto !py-4"
>
<div className="flex-1 text-left">
<h4 className="font-semibold text-gray-900 mb-1">New Author Profile</h4>
<p className="text-sm text-gray-600">Define a writing voice and style</p>
</div>
</Button>
<Button
onClick={() => navigate("/thinker/strategies")}
variant="outline"
size="lg"
startIcon={
<div className="size-8 rounded-lg bg-gradient-to-br from-[var(--color-purple)] to-[var(--color-purple-dark)] flex items-center justify-center text-white shadow-md">
<PlusIcon className="h-4 w-4" />
</div>
}
className="!justify-start !h-auto !py-4"
>
<div className="flex-1 text-left">
<h4 className="font-semibold text-gray-900 mb-1">New Strategy</h4>
<p className="text-sm text-gray-600">Build a content playbook</p>
</div>
</Button>
</div>
</ComponentCard>
{/* Info Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<ComponentCard title="How Thinker Works" desc="Understanding the strategic OS">
<div className="space-y-4">
<div className="flex gap-4">
<div className="flex-shrink-0 size-10 rounded-lg bg-gradient-to-br from-[var(--color-warning)] to-[var(--color-warning-dark)] flex items-center justify-center text-white shadow-md">
<BoltIcon className="h-5 w-5" />
</div>
<div>
<h4 className="font-semibold text-gray-900 mb-1">Centralized Control</h4>
<p className="text-sm text-gray-600">
Manage all AI prompts, author voices, and brand guidelines in one place. Changes sync automatically to all content generation.
</p>
</div>
</div>
<div className="flex gap-4">
<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">
<CheckCircleIcon className="h-5 w-5" />
</div>
<div>
<h4 className="font-semibold text-gray-900 mb-1">Version Control</h4>
<p className="text-sm text-gray-600">
Track changes to prompts and strategies with full version history. Roll back to previous versions when needed.
</p>
</div>
</div>
<div className="flex gap-4">
<div className="flex-shrink-0 size-10 rounded-lg bg-gradient-to-br from-[var(--color-success)] to-[var(--color-success-dark)] flex items-center justify-center text-white shadow-md">
<ShootingStarIcon className="h-5 w-5" />
</div>
<div>
<h4 className="font-semibold text-gray-900 mb-1">Automated Enforcement</h4>
<p className="text-sm text-gray-600">
Every piece of content automatically uses your defined prompts, author profiles, and brand guidelines.
</p>
</div>
</div>
</div>
</ComponentCard>
<ComponentCard title="Getting Started" desc="Quick guide to using Thinker">
<div className="space-y-3">
<div className="flex items-start gap-3">
<div className="flex-shrink-0 size-8 rounded-full bg-brand-500 text-white flex items-center justify-center font-bold text-sm">
1
</div>
<div>
<h4 className="font-semibold text-gray-900 mb-1">Create Author Profiles</h4>
<p className="text-sm text-gray-600">
Define writing voices and styles that match your brand. Each profile can have unique tone, structure, and guidelines.
</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="flex-shrink-0 size-8 rounded-full bg-success-500 text-white flex items-center justify-center font-bold text-sm">
2
</div>
<div>
<h4 className="font-semibold text-gray-900 mb-1">Build Prompt Library</h4>
<p className="text-sm text-gray-600">
Create reusable prompt templates for different content types. Use variables to make prompts dynamic and flexible.
</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="flex-shrink-0 size-8 rounded-full bg-purple-500 text-white flex items-center justify-center font-bold text-sm">
3
</div>
<div>
<h4 className="font-semibold text-gray-900 mb-1">Define Strategies</h4>
<p className="text-sm text-gray-600">
Create content playbooks that combine prompts, profiles, and guidelines. Apply strategies to specific clusters or content types.
</p>
</div>
</div>
</div>
</ComponentCard>
</div>
</div>
</>
);
}

View File

@@ -1,25 +0,0 @@
import UserMetaCard from "../components/UserProfile/UserMetaCard";
import UserInfoCard from "../components/UserProfile/UserInfoCard";
import UserAddressCard from "../components/UserProfile/UserAddressCard";
import PageMeta from "../components/common/PageMeta";
export default function UserProfiles() {
return (
<>
<PageMeta
title="React.js Profile Dashboard | TailAdmin - Next.js Admin Dashboard Template"
description="This is React.js Profile Dashboard page for TailAdmin - React.js Tailwind CSS Admin Dashboard Template"
/>
<div className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] lg:p-6">
<h3 className="mb-5 text-lg font-semibold text-gray-800 dark:text-white/90 lg:mb-7">
Profile
</h3>
<div className="space-y-6">
<UserMetaCard />
<UserInfoCard />
<UserAddressCard />
</div>
</div>
</>
);
}

View File

@@ -1,823 +0,0 @@
import { useEffect, useState, useMemo, lazy, Suspense } from "react";
import { Link, useNavigate } from "react-router-dom";
import PageMeta from "../../components/common/PageMeta";
import ComponentCard from "../../components/common/ComponentCard";
import { ProgressBar } from "../../components/ui/progress";
import { ApexOptions } from "apexcharts";
import EnhancedMetricCard from "../../components/dashboard/EnhancedMetricCard";
import PageHeader from "../../components/common/PageHeader";
const Chart = lazy(() => import("react-apexcharts").then((mod) => ({ default: mod.default })));
import {
FileTextIcon,
BoxIcon,
CheckCircleIcon,
ClockIcon,
PencilIcon,
BoltIcon,
ArrowRightIcon,
PaperPlaneIcon,
PlugInIcon,
} from "../../icons";
import {
fetchTasks,
fetchContent,
fetchContentImages,
fetchTaxonomies,
} from "../../services/api";
import { useSiteStore } from "../../store/siteStore";
import { useSectorStore } from "../../store/sectorStore";
interface WriterStats {
tasks: {
total: number;
byStatus: Record<string, number>;
pending: number;
inProgress: number;
completed: number;
avgWordCount: number;
totalWordCount: number;
};
content: {
total: number;
drafts: number;
published: number;
publishedToSite: number;
scheduledForPublish: number;
totalWordCount: number;
avgWordCount: number;
byContentType: Record<string, number>;
};
images: {
total: number;
generated: number;
pending: number;
failed: number;
byType: Record<string, number>;
};
workflow: {
tasksCreated: boolean;
contentGenerated: boolean;
imagesGenerated: boolean;
readyToPublish: boolean;
};
productivity: {
contentThisWeek: number;
contentThisMonth: number;
avgGenerationTime: number;
publishRate: number;
};
taxonomies: number;
attributes: number;
}
export default function WriterDashboard() {
const navigate = useNavigate();
const { activeSite } = useSiteStore();
const { activeSector } = useSectorStore();
const [stats, setStats] = useState<WriterStats | null>(null);
const [loading, setLoading] = useState(true);
const [lastUpdated, setLastUpdated] = useState<Date>(new Date());
const fetchDashboardData = async () => {
try {
setLoading(true);
const [tasksRes, contentRes, imagesRes, taxonomiesRes] = await Promise.all([
fetchTasks({ page_size: 1000, sector_id: activeSector?.id }),
fetchContent({ page_size: 1000, sector_id: activeSector?.id }),
fetchContentImages({ sector_id: activeSector?.id }),
fetchTaxonomies({ page_size: 1000, sector_id: activeSector?.id }),
]);
const tasks = tasksRes.results || [];
const tasksByStatus: Record<string, number> = {};
let pendingTasks = 0;
let inProgressTasks = 0;
let completedTasks = 0;
let totalWordCount = 0;
tasks.forEach(t => {
tasksByStatus[t.status || 'queued'] = (tasksByStatus[t.status || 'queued'] || 0) + 1;
if (t.status === 'queued') pendingTasks++;
else if (t.status === 'completed') completedTasks++;
if (t.word_count) totalWordCount += t.word_count;
});
const avgWordCount = tasks.length > 0 ? Math.round(totalWordCount / tasks.length) : 0;
const content = contentRes.results || [];
let drafts = 0;
let published = 0;
let publishedToSite = 0;
let scheduledForPublish = 0;
let contentTotalWordCount = 0;
const contentByType: Record<string, number> = {};
content.forEach(c => {
if (c.status === 'draft') drafts++;
else if (c.status === 'published') published++;
// Count site_status for external publishing metrics
if (c.site_status === 'published') publishedToSite++;
else if (c.site_status === 'scheduled') scheduledForPublish++;
if (c.word_count) contentTotalWordCount += c.word_count;
});
const contentAvgWordCount = content.length > 0 ? Math.round(contentTotalWordCount / content.length) : 0;
const images = imagesRes.results || [];
let generatedImages = 0;
let pendingImages = 0;
let failedImages = 0;
const imagesByType: Record<string, number> = {};
images.forEach(imgGroup => {
if (imgGroup.overall_status === 'complete') generatedImages++;
else if (imgGroup.overall_status === 'pending' || imgGroup.overall_status === 'partial') pendingImages++;
else if (imgGroup.overall_status === 'failed') failedImages++;
if (imgGroup.featured_image) {
imagesByType['featured'] = (imagesByType['featured'] || 0) + 1;
}
if (imgGroup.in_article_images && imgGroup.in_article_images.length > 0) {
imagesByType['in_article'] = (imagesByType['in_article'] || 0) + imgGroup.in_article_images.length;
}
});
const contentThisWeek = Math.floor(content.length * 0.3);
const contentThisMonth = Math.floor(content.length * 0.7);
const publishRate = content.length > 0 ? Math.round((published / content.length) * 100) : 0;
const taxonomies = taxonomiesRes.results || [];
const taxonomyCount = taxonomies.length;
// Note: Attributes are a subset of taxonomies with type 'product_attribute'
const attributeCount = taxonomies.filter(t => t.taxonomy_type === 'product_attribute').length;
setStats({
tasks: {
total: tasks.length,
byStatus: tasksByStatus,
pending: pendingTasks,
inProgress: inProgressTasks,
completed: completedTasks,
avgWordCount,
totalWordCount
},
content: {
total: content.length,
drafts,
published,
publishedToSite,
scheduledForPublish,
totalWordCount: contentTotalWordCount,
avgWordCount: contentAvgWordCount,
byContentType: contentByType
},
images: {
total: images.length,
generated: generatedImages,
pending: pendingImages,
failed: failedImages,
byType: imagesByType
},
workflow: {
tasksCreated: tasks.length > 0,
contentGenerated: content.length > 0,
imagesGenerated: generatedImages > 0,
readyToPublish: published > 0
},
productivity: {
contentThisWeek,
contentThisMonth,
avgGenerationTime: 0,
publishRate
},
taxonomies: taxonomyCount,
attributes: attributeCount,
});
setLastUpdated(new Date());
} catch (error) {
console.error('Error fetching dashboard data:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchDashboardData();
const interval = setInterval(fetchDashboardData, 30000);
return () => clearInterval(interval);
}, [activeSector?.id, activeSite?.id]);
const completionRate = useMemo(() => {
if (!stats || stats.tasks.total === 0) return 0;
return Math.round((stats.tasks.completed / stats.tasks.total) * 100);
}, [stats]);
const writerModules = [
{
title: "Tasks",
description: "Content writing tasks and assignments",
icon: FileTextIcon,
color: "from-[var(--color-primary)] to-[var(--color-primary-dark)]",
path: "/writer/tasks",
count: stats?.tasks.total || 0,
metric: `${stats?.tasks.completed || 0} completed`,
},
{
title: "Content",
description: "Generated content and drafts",
icon: PencilIcon,
color: "from-[var(--color-success)] to-[var(--color-success-dark)]",
path: "/writer/content",
count: stats?.content.total || 0,
metric: `${stats?.content.published || 0} published`,
},
{
title: "Images",
description: "Generated images and assets",
icon: BoxIcon,
color: "from-[var(--color-warning)] to-[var(--color-warning-dark)]",
path: "/writer/images",
count: stats?.images.generated || 0,
metric: `${stats?.images.pending || 0} pending`,
},
{
title: "Published to Site",
description: "Content published to external site",
icon: PaperPlaneIcon,
color: "from-[var(--color-purple)] to-[var(--color-purple-dark)]",
path: "/writer/published",
count: stats?.content.publishedToSite || 0,
metric: stats?.content.scheduledForPublish ? `${stats.content.scheduledForPublish} scheduled` : "None scheduled",
},
{
title: "Taxonomies",
description: "Manage content taxonomies",
icon: BoltIcon,
color: "from-[var(--color-info)] to-[var(--color-info-dark)]",
path: "/writer/taxonomies",
count: stats?.taxonomies || 0,
metric: `${stats?.taxonomies || 0} total`,
},
{
title: "Attributes",
description: "Manage content attributes",
icon: PlugInIcon,
color: "from-[var(--color-secondary)] to-[var(--color-secondary-dark)]",
path: "/writer/attributes",
count: stats?.attributes || 0,
metric: `${stats?.attributes || 0} total`,
},
];
const recentActivity = [
{
id: 1,
type: "Content Published",
description: `${stats?.content.published || 0} pieces published to site`,
timestamp: new Date(Date.now() - 30 * 60 * 1000),
icon: PaperPlaneIcon,
color: "text-success-600",
},
{
id: 2,
type: "Content Generated",
description: `${stats?.content.total || 0} content pieces created`,
timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000),
icon: PencilIcon,
color: "text-brand-600",
},
{
id: 3,
type: "Images Generated",
description: `${stats?.images.generated || 0} images created`,
timestamp: new Date(Date.now() - 4 * 60 * 60 * 1000),
icon: BoxIcon,
color: "text-warning-600",
},
];
const chartOptions: ApexOptions = {
chart: {
type: "area",
height: 300,
toolbar: { show: false },
zoom: { enabled: false },
},
stroke: {
curve: "smooth",
width: 3,
},
xaxis: {
categories: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
labels: { style: { colors: "var(--color-gray-500)" } },
},
yaxis: {
labels: { style: { colors: "var(--color-gray-500)" } },
},
legend: {
position: "top",
labels: { colors: "var(--color-gray-500)" },
},
colors: ["var(--color-primary)", "var(--color-success)", "var(--color-warning)"],
grid: {
borderColor: "var(--color-gray-200)",
},
fill: {
type: "gradient",
gradient: {
opacityFrom: 0.6,
opacityTo: 0.1,
},
},
};
const chartSeries = [
{
name: "Content Created",
data: [12, 19, 15, 25, 22, 18, 24],
},
{
name: "Tasks Completed",
data: [8, 12, 10, 15, 14, 11, 16],
},
{
name: "Images Generated",
data: [5, 8, 6, 10, 9, 7, 11],
},
];
const tasksStatusChart = useMemo(() => {
if (!stats) return null;
const options: ApexOptions = {
chart: {
type: 'donut',
fontFamily: 'Outfit, sans-serif',
toolbar: { show: false }
},
labels: Object.keys(stats.tasks.byStatus).filter(key => stats.tasks.byStatus[key] > 0),
colors: ['var(--color-primary)', 'var(--color-warning)', 'var(--color-success)', 'var(--color-danger)', 'var(--color-purple)'],
legend: {
position: 'bottom',
fontFamily: 'Outfit',
show: true
},
dataLabels: {
enabled: false
},
plotOptions: {
pie: {
donut: {
size: '70%',
labels: {
show: true,
name: { show: false },
value: {
show: true,
fontSize: '24px',
fontWeight: 700,
color: 'var(--color-primary)',
fontFamily: 'Outfit',
formatter: () => {
const total = Object.values(stats.tasks.byStatus).reduce((a: number, b: number) => a + b, 0);
return total > 0 ? total.toString() : '0';
}
},
total: { show: false }
}
}
}
}
};
const series = Object.keys(stats.tasks.byStatus)
.filter(key => stats.tasks.byStatus[key] > 0)
.map(key => stats.tasks.byStatus[key]);
return { options, series };
}, [stats]);
const contentStatusChart = useMemo(() => {
if (!stats) return null;
const options: ApexOptions = {
chart: {
type: 'bar',
fontFamily: 'Outfit, sans-serif',
toolbar: { show: false },
height: 300
},
colors: ['var(--color-primary)', 'var(--color-warning)', 'var(--color-success)'],
plotOptions: {
bar: {
horizontal: false,
columnWidth: '55%',
borderRadius: 5
}
},
dataLabels: {
enabled: true
},
xaxis: {
categories: ['Drafts', 'Published'],
labels: {
style: {
fontFamily: 'Outfit'
}
}
},
yaxis: {
labels: {
style: {
fontFamily: 'Outfit'
}
}
},
grid: {
strokeDashArray: 4
}
};
const series = [{
name: 'Content',
data: [stats.content.drafts, stats.content.published]
}];
return { options, series };
}, [stats]);
const formatTimeAgo = (date: Date) => {
const minutes = Math.floor((Date.now() - date.getTime()) / 60000);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
return `${days}d ago`;
};
if (loading && !stats) {
return (
<>
<PageMeta title="Writer Dashboard - IGNY8" description="Content creation overview" />
<div className="flex items-center justify-center h-96">
<div className="text-center">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-4 border-brand-500 border-t-transparent"></div>
<p className="mt-4 text-gray-600 dark:text-gray-400">Loading dashboard data...</p>
</div>
</div>
</>
);
}
if (!stats && !loading) {
return (
<>
<PageMeta title="Writer Dashboard - IGNY8" description="Content creation overview" />
<div className="text-center py-12">
<p className="text-gray-600 dark:text-gray-400">
{activeSector ? 'No data available for the selected sector.' : 'No data available. Select a sector or wait for data to load.'}
</p>
</div>
</>
);
}
if (!stats) return null;
return (
<>
<PageMeta title="Content Creation Dashboard - IGNY8" description="Track your writing progress and productivity" />
<PageHeader
title="Content Creation Dashboard"
lastUpdated={lastUpdated}
showRefresh={true}
onRefresh={fetchDashboardData}
/>
<div className="space-y-6">
{/* Key Metrics */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<EnhancedMetricCard
title="Total Tasks"
value={stats.tasks.total}
subtitle={`${stats.tasks.completed} completed • ${stats.tasks.pending} pending`}
icon={<FileTextIcon className="size-6" />}
accentColor="blue"
trend={0}
href="/writer/tasks"
/>
<EnhancedMetricCard
title="Content Pieces"
value={stats.content.total}
subtitle={`${stats.content.published} published • ${stats.content.drafts} drafts`}
icon={<PencilIcon className="size-6" />}
accentColor="green"
trend={0}
href="/writer/content"
/>
<EnhancedMetricCard
title="Images Generated"
value={stats.images.generated}
subtitle={`${stats.images.total} total • ${stats.images.pending} pending`}
icon={<BoxIcon className="size-6" />}
accentColor="orange"
trend={0}
href="/writer/images"
/>
<EnhancedMetricCard
title="Publish Rate"
value={`${stats.productivity.publishRate}%`}
subtitle={`${stats.content.published} of ${stats.content.total} published`}
icon={<PaperPlaneIcon className="size-6" />}
accentColor="purple"
trend={0}
href="/writer/published"
/>
</div>
{/* Writer Modules */}
<ComponentCard title="Writer Modules" desc="Access all content creation tools and features">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{writerModules.map((module) => {
const Icon = module.icon;
return (
<Link
key={module.title}
to={module.path}
className="rounded-2xl border-2 border-gray-200 bg-white p-6 hover:shadow-xl hover:-translate-y-1 transition-all group"
>
<div className="flex items-start justify-between mb-4">
<div className={`inline-flex size-14 rounded-xl bg-gradient-to-br ${module.color} items-center justify-center text-white shadow-lg`}>
<Icon className="h-7 w-7" />
</div>
</div>
<h3 className="text-lg font-bold text-gray-900 mb-2">{module.title}</h3>
<p className="text-sm text-gray-600 mb-4">{module.description}</p>
<div className="flex items-center justify-between">
<div>
<div className="text-2xl font-bold text-gray-900">{module.count}</div>
<div className="text-xs text-gray-500">{module.metric}</div>
</div>
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-primary)] group-hover:translate-x-1 transition" />
</div>
</Link>
);
})}
</div>
</ComponentCard>
{/* Activity Chart & Recent Activity */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<ComponentCard title="Content Creation Activity" desc="Tasks, content, and images over the past week">
<Suspense fallback={<div className="h-[300px] flex items-center justify-center">Loading chart...</div>}>
<Chart options={chartOptions} series={chartSeries} type="area" height={300} />
</Suspense>
</ComponentCard>
<ComponentCard title="Recent Activity" desc="Latest content creation actions and updates">
<div className="space-y-4">
{recentActivity.map((activity) => {
const Icon = activity.icon;
return (
<div
key={activity.id}
className="flex items-center gap-4 p-4 rounded-lg border border-gray-200 bg-white hover:shadow-md transition"
>
<div className={`size-10 rounded-lg bg-gradient-to-br from-gray-100 to-gray-200 flex items-center justify-center ${activity.color}`}>
<Icon className="h-5 w-5" />
</div>
<div className="flex-1">
<div className="flex items-center justify-between mb-1">
<h4 className="font-semibold text-gray-900">{activity.type}</h4>
<span className="text-xs text-gray-500">{formatTimeAgo(activity.timestamp)}</span>
</div>
<p className="text-sm text-gray-600">{activity.description}</p>
</div>
</div>
);
})}
</div>
</ComponentCard>
</div>
{/* Charts */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{tasksStatusChart && (
<ComponentCard title="Tasks by Status" desc="Task distribution across statuses">
<Suspense fallback={<div className="h-[300px] flex items-center justify-center">Loading chart...</div>}>
<Chart
options={tasksStatusChart.options}
series={tasksStatusChart.series}
type="donut"
height={300}
/>
</Suspense>
</ComponentCard>
)}
{contentStatusChart && (
<ComponentCard title="Content by Status" desc="Distribution across workflow stages">
<Suspense fallback={<div className="h-[300px] flex items-center justify-center">Loading chart...</div>}>
<Chart
options={contentStatusChart.options}
series={contentStatusChart.series}
type="bar"
height={300}
/>
</Suspense>
</ComponentCard>
)}
</div>
{/* Productivity Metrics */}
<ComponentCard title="Productivity Metrics" desc="Content creation performance tracking">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Task Completion</span>
<span className="text-sm font-semibold text-gray-800 dark:text-white/90">{completionRate}%</span>
</div>
<ProgressBar value={completionRate} color="primary" size="md" />
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{stats.tasks.completed} of {stats.tasks.total} tasks completed
</p>
</div>
<div>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Publish Rate</span>
<span className="text-sm font-semibold text-gray-800 dark:text-white/90">{stats.productivity.publishRate}%</span>
</div>
<ProgressBar value={stats.productivity.publishRate} color="success" size="md" />
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{stats.content.published} of {stats.content.total} content published
</p>
</div>
<div>
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Image Generation</span>
<span className="text-sm font-semibold text-gray-800 dark:text-white/90">
{stats.images.total > 0 ? Math.round((stats.images.generated / stats.images.total) * 100) : 0}%
</span>
</div>
<ProgressBar
value={stats.images.total > 0 ? Math.round((stats.images.generated / stats.images.total) * 100) : 0}
color="warning"
size="md"
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{stats.images.generated} of {stats.images.total} images generated
</p>
</div>
</div>
</ComponentCard>
{/* Quick Actions */}
<ComponentCard title="Quick Actions" desc="Common content creation tasks and shortcuts">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Link
to="/writer/tasks"
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"
>
<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">
<FileTextIcon className="h-6 w-6" />
</div>
<div className="flex-1 text-left">
<h4 className="font-semibold text-gray-900 mb-1">Create Task</h4>
<p className="text-sm text-gray-600">New writing task</p>
</div>
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-primary)] transition" />
</Link>
<Link
to="/writer/content"
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-success-500 hover:shadow-lg transition-all group"
>
<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">
<PencilIcon className="h-6 w-6" />
</div>
<div className="flex-1 text-left">
<h4 className="font-semibold text-gray-900 mb-1">Generate Content</h4>
<p className="text-sm text-gray-600">AI content creation</p>
</div>
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-success-500 transition" />
</Link>
<Link
to="/writer/images"
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-warning-500 hover:shadow-lg transition-all group"
>
<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">
<BoxIcon className="h-6 w-6" />
</div>
<div className="flex-1 text-left">
<h4 className="font-semibold text-gray-900 mb-1">Generate Images</h4>
<p className="text-sm text-gray-600">Create visuals</p>
</div>
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-warning-500 transition" />
</Link>
<Link
to="/writer/published"
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-purple-500 hover:shadow-lg transition-all group"
>
<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">
<PaperPlaneIcon className="h-6 w-6" />
</div>
<div className="flex-1 text-left">
<h4 className="font-semibold text-gray-900 mb-1">Publish Content</h4>
<p className="text-sm text-gray-600">Publish to Site</p>
</div>
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-purple-500 transition" />
</Link>
</div>
</ComponentCard>
{/* Info Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<ComponentCard title="How Writer Works" desc="Understanding the content creation workflow">
<div className="space-y-4">
<div className="flex gap-4">
<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">
<FileTextIcon className="h-5 w-5" />
</div>
<div>
<h4 className="font-semibold text-gray-900 mb-1">Task Creation</h4>
<p className="text-sm text-gray-600">
Create writing tasks from content ideas. Each task includes target keywords, outline, and word count requirements.
</p>
</div>
</div>
<div className="flex gap-4">
<div className="flex-shrink-0 size-10 rounded-lg bg-gradient-to-br from-[var(--color-success)] to-[var(--color-success-dark)] flex items-center justify-center text-white shadow-md">
<PencilIcon className="h-5 w-5" />
</div>
<div>
<h4 className="font-semibold text-gray-900 mb-1">AI Content Generation</h4>
<p className="text-sm text-gray-600">
Generate full content pieces using AI. Content is created based on your prompts, author profiles, and brand guidelines.
</p>
</div>
</div>
<div className="flex gap-4">
<div className="flex-shrink-0 size-10 rounded-lg bg-gradient-to-br from-[var(--color-warning)] to-[var(--color-warning-dark)] flex items-center justify-center text-white shadow-md">
<BoxIcon className="h-5 w-5" />
</div>
<div>
<h4 className="font-semibold text-gray-900 mb-1">Image Generation</h4>
<p className="text-sm text-gray-600">
Automatically generate featured images and in-article images for your content. Images are optimized for SEO and engagement.
</p>
</div>
</div>
</div>
</ComponentCard>
<ComponentCard title="Getting Started" desc="Quick guide to using Writer">
<div className="space-y-3">
<div className="flex items-start gap-3">
<div className="flex-shrink-0 size-8 rounded-full bg-brand-500 text-white flex items-center justify-center font-bold text-sm">
1
</div>
<div>
<h4 className="font-semibold text-gray-900 mb-1">Create Tasks</h4>
<p className="text-sm text-gray-600">
Start by creating writing tasks from content ideas in the Planner module. Tasks define what content needs to be written.
</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="flex-shrink-0 size-8 rounded-full bg-success-500 text-white flex items-center justify-center font-bold text-sm">
2
</div>
<div>
<h4 className="font-semibold text-gray-900 mb-1">Generate Content</h4>
<p className="text-sm text-gray-600">
Use AI to generate content from tasks. Review and edit generated content before publishing.
</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="flex-shrink-0 size-8 rounded-full bg-warning-500 text-white flex items-center justify-center font-bold text-sm">
3
</div>
<div>
<h4 className="font-semibold text-gray-900 mb-1">Publish</h4>
<p className="text-sm text-gray-600">
Once content is reviewed and images are generated, publish directly to WordPress or export for manual publishing.
</p>
</div>
</div>
</div>
</ComponentCard>
</div>
</div>
</>
);
}

View File

@@ -1,13 +0,0 @@
/**
* Drafts Page - Filtered Tasks with status='draft'
* Consistent with Keywords page layout, structure and design
*/
import Tasks from './Tasks';
export default function Drafts() {
// Drafts is just Tasks with status='draft' filter applied
// For now, we'll use the Tasks component but could enhance it later
// to show only draft status tasks by default
return <Tasks />;
}

View File

@@ -21,6 +21,7 @@ import ComponentCard from '../../components/common/ComponentCard';
import { Modal } from '../../components/ui/modal';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { useAuthStore } from '../../store/authStore';
import { usePageLoading } from '../../context/PageLoadingContext';
import {
getAccountSettings,
updateAccountSettings,
@@ -46,9 +47,9 @@ export default function AccountSettingsPage() {
const toast = useToast();
const location = useLocation();
const { user, refreshUser } = useAuthStore();
const { startLoading, stopLoading } = usePageLoading();
// Derive active tab from URL path
const activeTab = getTabFromPath(location.pathname);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string>('');
const [success, setSuccess] = useState<string>('');
@@ -122,7 +123,7 @@ export default function AccountSettingsPage() {
const loadData = async () => {
try {
setLoading(true);
startLoading('Loading settings...');
const accountData = await getAccountSettings();
setSettings(accountData);
setAccountForm({
@@ -139,7 +140,7 @@ export default function AccountSettingsPage() {
} catch (err: any) {
setError(err.message || 'Failed to load settings');
} finally {
setLoading(false);
stopLoading();
}
};
@@ -269,26 +270,6 @@ export default function AccountSettingsPage() {
{ id: 'team' as TabType, label: 'Team', icon: <UsersIcon className="w-4 h-4" /> },
];
if (loading) {
return (
<>
<PageMeta title="Account Settings" description="Manage your account, profile, and team" />
<PageHeader
title="Account Settings"
badge={{ icon: <SettingsIcon className="w-4 h-4" />, color: 'blue' }}
/>
<div className="p-6">
<div className="flex items-center justify-center h-64">
<div className="flex flex-col items-center gap-3">
<Loader2Icon className="w-8 h-8 animate-spin text-[var(--color-brand-500)]" />
<div className="text-gray-500 dark:text-gray-400">Loading settings...</div>
</div>
</div>
</div>
</>
);
}
// Page titles based on active tab
const pageTitles = {
account: { title: 'Account Information', description: 'Manage your organization and billing information' },
@@ -305,7 +286,6 @@ export default function AccountSettingsPage() {
badge={{ icon: <SettingsIcon className="w-4 h-4" />, color: 'blue' }}
parent="Account Settings"
/>
<div className="p-6">
{/* Tab Content */}
{/* Account Tab */}
{activeTab === 'account' && (

View File

@@ -19,6 +19,7 @@ import Select from '../../components/form/Select';
import PageMeta from '../../components/common/PageMeta';
import PageHeader from '../../components/common/PageHeader';
import { useNotificationStore } from '../../store/notificationStore';
import { usePageLoading } from '../../context/PageLoadingContext';
import type { NotificationAPI } from '../../services/notifications.api';
import { deleteNotification as deleteNotificationAPI } from '../../services/notifications.api';
@@ -30,7 +31,7 @@ interface FilterState {
export default function NotificationsPage() {
const [apiNotifications, setApiNotifications] = useState<NotificationAPI[]>([]);
const [loading, setLoading] = useState(true);
const { startLoading, stopLoading } = usePageLoading();
const {
unreadCount,
@@ -53,7 +54,7 @@ export default function NotificationsPage() {
}, []);
const loadNotifications = async () => {
setLoading(true);
startLoading('Loading notifications...');
try {
// Import here to avoid circular dependencies
const { fetchNotifications } = await import('../../services/notifications.api');
@@ -63,7 +64,7 @@ export default function NotificationsPage() {
} catch (error) {
console.error('Failed to load notifications:', error);
} finally {
setLoading(false);
stopLoading();
}
};
@@ -313,11 +314,7 @@ export default function NotificationsPage() {
{/* Notifications List */}
<Card className="overflow-hidden">
{loading ? (
<div className="flex items-center justify-center p-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-brand-600"></div>
</div>
) : filteredNotifications.length === 0 ? (
{filteredNotifications.length === 0 ? (
<div className="text-center p-12">
<BellIcon className="w-12 h-12 text-gray-400 mx-auto mb-3" />
<p className="text-gray-600 dark:text-gray-400">

View File

@@ -24,6 +24,7 @@ import CreditCostBreakdownPanel from '../../components/billing/CreditCostBreakdo
// import CreditCostsPanel from '../../components/billing/CreditCostsPanel'; // Hidden from regular users
// import UsageLimitsPanel from '../../components/billing/UsageLimitsPanel'; // Moved to UsageAnalyticsPage
import { convertToPricingPlan } from '../../utils/pricingHelpers';
import { usePageLoading } from '../../context/PageLoadingContext';
import {
getCreditBalance,
getCreditPackages,
@@ -63,7 +64,7 @@ export default function PlansAndBillingPage() {
const location = useLocation();
// Derive active tab from URL path
const activeTab = getTabFromPath(location.pathname);
const [loading, setLoading] = useState(true);
const { startLoading, stopLoading } = usePageLoading();
const [error, setError] = useState<string>('');
const [planLoadingId, setPlanLoadingId] = useState<number | null>(null);
const [purchaseLoadingId, setPurchaseLoadingId] = useState<number | null>(null);
@@ -109,7 +110,7 @@ export default function PlansAndBillingPage() {
const loadData = async (allowRetry = true) => {
try {
setLoading(true);
startLoading('Loading billing data...');
// Fetch in controlled sequence to avoid burst 429s on auth/system scopes
const balanceData = await getCreditBalance();
@@ -213,7 +214,7 @@ export default function PlansAndBillingPage() {
console.error('Billing load error:', err);
}
} finally {
setLoading(false);
stopLoading();
}
};
@@ -267,7 +268,6 @@ export default function PlansAndBillingPage() {
handleBillingError(err, 'Failed to purchase credits');
} finally {
setPurchaseLoadingId(null);
setLoading(false);
}
};
@@ -340,23 +340,6 @@ export default function PlansAndBillingPage() {
}
};
if (loading) {
return (
<>
<PageMeta title="Plans & Billing" description="Manage your subscription and billing" />
<PageHeader
title="Plans & Billing"
badge={{ icon: <CreditCardIcon className="w-4 h-4" />, color: 'blue' }}
/>
<div className="p-6">
<div className="flex items-center justify-center h-64">
<Loader2Icon className="w-8 h-8 animate-spin text-[var(--color-brand-500)]" />
</div>
</div>
</>
);
}
const currentSubscription = subscriptions.find((sub) => sub.status === 'active') || subscriptions[0];
const currentPlanId = typeof currentSubscription?.plan === 'object' ? currentSubscription.plan.id : currentSubscription?.plan;
// Fallback to account plan if subscription is missing
@@ -384,7 +367,7 @@ export default function PlansAndBillingPage() {
badge={{ icon: <CreditCardIcon className="w-4 h-4" />, color: 'blue' }}
parent="Plans & Billing"
/>
<div className="p-6">
{/* Activation / pending payment notice */}
{!hasActivePlan && (
<div className="mb-4 p-4 rounded-lg border border-warning-200 bg-warning-50 text-warning-800 dark:border-warning-800 dark:bg-warning-900/20 dark:text-warning-200">
@@ -893,7 +876,6 @@ export default function PlansAndBillingPage() {
</div>
</div>
)}
</div>
</>
);
}

View File

@@ -97,7 +97,6 @@ export default function UsageAnalyticsPage() {
badge={{ icon: <TrendingUpIcon className="w-4 h-4" />, color: 'blue' }}
parent="Usage & Analytics"
/>
<div className="p-6">
{/* Quick Stats Overview */}
{creditBalance && (
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">

View File

@@ -1,172 +0,0 @@
/**
* User Profile Settings Page
* Manage personal profile settings
*/
import { useState } from 'react';
import { SaveIcon, UserIcon, MailIcon, LockIcon, Loader2Icon } from '../../icons';
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
import InputField from '../../components/form/input/InputField';
import Select from '../../components/form/Select';
import Checkbox from '../../components/form/input/Checkbox';
export default function ProfileSettingsPage() {
const [saving, setSaving] = useState(false);
const [profile, setProfile] = useState({
firstName: 'John',
lastName: 'Doe',
email: 'john@example.com',
phone: '+1 234 567 8900',
timezone: 'America/New_York',
language: 'en',
emailNotifications: true,
marketingEmails: false,
});
const handleSave = async () => {
setSaving(true);
await new Promise(resolve => setTimeout(resolve, 1000));
setSaving(false);
};
return (
<div className="p-6 max-w-4xl">
<div className="mb-6 flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
<UserIcon className="w-6 h-6" />
Your Profile
</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Update your personal settings - Your name, preferences, and notification choices
</p>
</div>
<Button
onClick={handleSave}
disabled={saving}
variant="primary"
tone="brand"
>
{saving ? <Loader2Icon className="w-4 h-4 animate-spin" /> : <SaveIcon className="w-4 h-4" />}
{saving ? 'Saving...' : '✓ Save My Settings'}
</Button>
</div>
<div className="space-y-6">
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4">About You</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<InputField
type="text"
label="First Name"
value={profile.firstName}
onChange={(e) => setProfile({ ...profile, firstName: e.target.value })}
/>
</div>
<div>
<InputField
type="text"
label="Last Name"
value={profile.lastName}
onChange={(e) => setProfile({ ...profile, lastName: e.target.value })}
/>
</div>
<div>
<InputField
type="email"
label="Email"
value={profile.email}
onChange={(e) => setProfile({ ...profile, email: e.target.value })}
/>
</div>
<div>
<InputField
type="tel"
label="Phone Number (optional)"
value={profile.phone}
onChange={(e) => setProfile({ ...profile, phone: e.target.value })}
/>
</div>
</div>
</Card>
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4">How You Like It</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Select
label="Your Timezone"
options={[
{ value: 'America/New_York', label: 'Eastern Time' },
{ value: 'America/Chicago', label: 'Central Time' },
{ value: 'America/Denver', label: 'Mountain Time' },
{ value: 'America/Los_Angeles', label: 'Pacific Time' },
{ value: 'UTC', label: 'UTC' },
]}
defaultValue={profile.timezone}
onChange={(val) => setProfile({ ...profile, timezone: val })}
/>
</div>
<div>
<Select
label="Language"
options={[
{ value: 'en', label: 'English' },
{ value: 'es', label: 'Spanish' },
{ value: 'fr', label: 'French' },
]}
defaultValue={profile.language}
onChange={(val) => setProfile({ ...profile, language: val })}
/>
</div>
</div>
</Card>
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4">What You Want to Hear About</h2>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
Choose what emails you want to receive:
</p>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<div className="font-medium">Important Updates</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
Get notified about important changes to your account
</div>
</div>
<Checkbox
checked={profile.emailNotifications}
onChange={(checked) => setProfile({ ...profile, emailNotifications: checked })}
/>
</div>
<div className="flex items-center justify-between">
<div>
<div className="font-medium">Tips & Product Updates (optional)</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
Hear about new features and content tips
</div>
</div>
<Checkbox
checked={profile.marketingEmails}
onChange={(checked) => setProfile({ ...profile, marketingEmails: checked })}
/>
</div>
</div>
</Card>
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
<LockIcon className="w-5 h-5" />
Security
</h2>
<Button variant="outline" tone="neutral">
Change Password
</Button>
</Card>
</div>
</div>
);
}

View File

@@ -1,165 +0,0 @@
## IGNY8 AI & Configuration Settings Report
---
### 1. AI Mode Configuration
**Architecture**: Two-tier database-driven model configuration system
#### AIModelConfig Model (Primary)
Stores all AI model configurations in database, replacing legacy hardcoded constants.
**Model Types**:
- Text Generation ✅
- Image Generation ✅
**Supported Providers**:
- OpenAI ✅
- Runware ✅
**Key Configuration Fields**:
| Category | Fields |
|----------|--------|
| **Identity** | `model_id`, `display_name`, `model_type`, `provider` |
| **Text Pricing** | `input_token_rate`, `output_token_rate` (per 1M tokens in USD) |
| **Text Limits** | `max_input_tokens`, `max_output_tokens` |
| **Image Pricing** | `cost_per_image` (fixed USD per image) |
| **Image Config** | `available_sizes` (JSON array of valid dimensions) |
| **Status** | `is_active`, `is_default`, `sort_order` |
| **Metadata** | `notes`, `release_date`, `deprecation_date` |
**Seeded Models**:
- **OpenAI Text**: gpt-4o-mini, gpt-4o, gpt-5.1(default)
- **OpenAI Image**: dall-e-3 (default)
- **Runware**: runware:97@1, google:4@2
---
### 2. Global Integration Settings
**Model**: `GlobalIntegrationSettings` (Singleton - always pk=1)
Stores **platform-wide** API keys and default settings used by ALL accounts.
| Provider | API Key Field | Default Model | Parameters |
|----------|---------------|---------------|------------|
| **OpenAI** | `openai_api_key` | gpt-5.1 | temperature: 0.7, max_tokens: 8192 |
| **DALL-E** | `dalle_api_key` | dall-e-3 | size: 1024x1024 |
| **Runware** | `runware_api_key` | runware:97@1 & google:4@2 | — |
**Default Provider Settings**:
- `default_text_provider`: 'openai'
- `default_image_service`: 'openai'
**Universal Image Settings**:
- `image_quality`: standard/hd
- `image_style`: photorealistic, illustration, etc.
- `max_in_article_images`: Default 2
**Critical Security Rule**: API keys exist ONLY in GlobalIntegrationSettings - never stored at account/user level.
---
### 4. User-Specific Record Creation (Save Mechanism)
**Three-Tier Hierarchy**:
```
Global (Platform) → Account (Tenant) → User (Personal)
```
#### Models Involved
| Model | Scope | Unique Key |
|-------|-------|------------|
| `GlobalIntegrationSettings` | Platform-wide | Singleton (pk=1) |
| `AccountSettings` | Per-tenant | account + key |
| `IntegrationSettings` | Per-tenant overrides | account + integration_type |
| `UserSettings` | Per-user preferences | user + account + key |
#### Save Flow When User Changes Config
1. **Frontend** calls POST/PUT to `/api/v1/system/settings/user/`
2. **Backend** ViewSet extracts user and account from authenticated request
3. **Check existing**: Query for existing setting with same user + account + key
4. **Create or Update**:
- If not exists → `serializer.save(user=user, account=account)` creates new record
- If exists → Updates the `value` JSON field
5. **Validation**: Schema validation runs against `SETTINGS_SCHEMAS` before save
6. **Response** returns the saved setting object
#### Integration Override Pattern
For AI/integration settings specifically:
1. User changes model/temperature (NOT API keys)
2. System strips any API key fields from request (security)
3. `IntegrationSettings.objects.get_or_create(account=account, integration_type=type)`
4. Only allowed override fields saved in `config` JSON field
5. On read, system merges: Global defaults → Account overrides
---
### 6. Image Generation: Internal Cost vs Customer Credit Allocation
#### Internal Cost (What Platform Pays to Providers)
| Model | Provider | Cost Per Image (USD) |
|-------|----------|---------------------|
| dall-e-3 | OpenAI | $0.05 |
| runware:97@1 | Runware - Hi Dream Full | ~$0.013 |
| google:4@2 | Runware - Google Nano Banaan | ~$0.15 |
**Storage**: AIModelConfig.`cost_per_image` field + legacy `IMAGE_MODEL_RATES` constants
!!! This need to be fixed rates tobe laoded and used form configured AI Models !! not from hard coded location
#### Customer Credit Cost (What Customer Pays)
| Operation | Credits Charged | Price per Credit | Min Charge |
|-----------|-----------------|------------------|------------|
| Image Generation | 5 credits | $0.02 | $0.10 |
| Image Prompt Extraction | 2 credits | $0.01 | $0.02 |
!!!morre robust image gneartion csoting and pricing mecahnishm required, withotu long chains or workarounds!!!
**Configuration Model**: `CreditCostConfig`
- `tokens_per_credit`: 50 (image gen uses fewer tokens per credit = higher cost)
- `min_credits`: 5
- `price_per_credit_usd`: $0.02
#### Margin Calculation
| Metric | DALL-E 3 Example |
|--------|------------------|
| Provider Cost | $0.040 |
| Customer Charge | $0.10 (5 credits × $0.02) |
| **Margin** | $0.06 (60% of customer charge) |
| **Markup** | ~150% |
**Flow**:
1. **Before AI call**: Calculate required credits based on image count
2. **Check balance**: Verify account has sufficient credits
3. **Deduct credits**: Remove from balance, log transaction
4. **Execute**: Make AI provider API call
5. **Track**: `CreditUsageLog` stores both `credits_used` (customer) and `cost_usd` (actual)
**Revenue Analytics** queries `CreditUsageLog` to calculate:
- Total revenue = Σ(credits_used × credit_price)
- Total cost = Σ(cost_usd from provider)
- Margin = Revenue - Cost
---
### Summary
The IGNY8 platform implements a sophisticated multi-tier configuration system:
- **AI configuration** is database-driven with fallback to legacy constants
- **Global settings** hold platform API keys; accounts only override model/parameters
- **User settings** create per-user records keyed by user + account + key combination
- **Credit system** charges customers a markup (~150%) over actual provider costs
- **Several fields are deprecated** including `mobile_image_size`, `reference_id`, and the `get_model()` method

View File

@@ -1,218 +0,0 @@
## IGNY8 AI & Configuration Settings Report
---
### 1. AI Mode Configuration
**Architecture**: Two-tier database-driven model configuration system
#### AIModelConfig Model (Primary)
Stores all AI model configurations in database, replacing legacy hardcoded constants.
**Model Types**:
- Text Generation
- Image Generation
- Embedding
**Supported Providers**:
- OpenAI
- Anthropic
- Runware
- Google
**Key Configuration Fields**:
| Category | Fields |
|----------|--------|
| **Identity** | `model_id`, `display_name`, `model_type`, `provider` |
| **Text Pricing** | `input_token_rate`, `output_token_rate` (per 1M tokens in USD) |
| **Text Limits** | `max_input_tokens`, `max_output_tokens` |
| **Image Pricing** | `cost_per_image` (fixed USD per image) |
| **Image Config** | `available_sizes` (JSON array of valid dimensions) |
| **Capabilities** | `supports_json_mode`, `supports_vision`, `supports_tools` |
| **Status** | `is_active`, `is_default`, `sort_order` |
| **Metadata** | `notes`, `release_date`, `deprecation_date` |
**Seeded Models**:
- **OpenAI Text**: gpt-4.1 (default), gpt-4o-mini, gpt-4o, gpt-5.1, gpt-5.2
- **Anthropic Text**: claude-3-5-sonnet, claude-3-opus, claude-3-haiku variants
- **OpenAI Image**: dall-e-3 (default), dall-e-2, gpt-image-1, gpt-image-1-mini
- **Runware/Bria**: runware:100@1, bria-2.3, bria-2.3-fast, bria-2.2
---
### 2. Global Integration Settings
**Model**: `GlobalIntegrationSettings` (Singleton - always pk=1)
Stores **platform-wide** API keys and default settings used by ALL accounts.
| Provider | API Key Field | Default Model | Parameters |
|----------|---------------|---------------|------------|
| **OpenAI** | `openai_api_key` | gpt-4o-mini | temperature: 0.7, max_tokens: 8192 |
| **Anthropic** | `anthropic_api_key` | claude-3-5-sonnet-20241022 | temperature: 0.7, max_tokens: 8192 |
| **DALL-E** | `dalle_api_key` | dall-e-3 | size: 1024x1024 |
| **Runware** | `runware_api_key` | runware:97@1 | — |
| **Bria** | `bria_api_key` | bria-2.3 | — |
**Default Provider Settings**:
- `default_text_provider`: 'openai' or 'anthropic'
- `default_image_service`: 'openai' or 'runware'
**Universal Image Settings**:
- `image_quality`: standard/hd
- `image_style`: photorealistic, illustration, etc.
- `max_in_article_images`: Default 2
- `desktop_image_size`: Default 1024x1024
**Critical Security Rule**: API keys exist ONLY in GlobalIntegrationSettings - never stored at account/user level.
---
### 3. Frontend Configuration Settings Panel
**Structure**: Three main setting hierarchies
#### Account Section (`/account/*`)
| Page | Tabs | Purpose |
|------|------|---------|
| Account Settings | Account, Profile, Team | User account management |
| Content Settings | Content, Publishing, Images | Content creation workflow |
| Plans & Billing | Plan, Upgrade, Invoices | Subscription management |
| Usage Analytics | Overview, Credits, Activity | Usage tracking |
#### Settings Section (`/settings/*`)
| Page | Purpose |
|------|---------|
| General | Table settings, app preferences |
| System | Global platform settings |
| AI Settings | AI model configuration |
| Integration | API integrations (Admin only) |
| Publishing | Publishing destinations & rules |
#### Site-Level Settings (`/sites/:id/settings`)
**Tabs**: general, content-generation, image-settings, integrations, publishing, content-types
**State Management**: Zustand store with persistence middleware (`useSettingsStore`)
**Available Settings Keys**:
- `table_settings`: records_per_page, default_sort, sort_direction
- `user_preferences`: theme, language, notifications
- `ai_settings`: model overrides, temperature, max_tokens
- `planner_automation`: automation rules
- `writer_automation`: content generation rules
---
### 4. User-Specific Record Creation (Save Mechanism)
**Three-Tier Hierarchy**:
```
Global (Platform) → Account (Tenant) → User (Personal)
```
#### Models Involved
| Model | Scope | Unique Key |
|-------|-------|------------|
| `GlobalIntegrationSettings` | Platform-wide | Singleton (pk=1) |
| `AccountSettings` | Per-tenant | account + key |
| `IntegrationSettings` | Per-tenant overrides | account + integration_type |
| `UserSettings` | Per-user preferences | user + account + key |
#### Save Flow When User Changes Config
1. **Frontend** calls POST/PUT to `/api/v1/system/settings/user/`
2. **Backend** ViewSet extracts user and account from authenticated request
3. **Check existing**: Query for existing setting with same user + account + key
4. **Create or Update**:
- If not exists → `serializer.save(user=user, account=account)` creates new record
- If exists → Updates the `value` JSON field
5. **Validation**: Schema validation runs against `SETTINGS_SCHEMAS` before save
6. **Response** returns the saved setting object
#### Integration Override Pattern
For AI/integration settings specifically:
1. User changes model/temperature (NOT API keys)
2. System strips any API key fields from request (security)
3. `IntegrationSettings.objects.get_or_create(account=account, integration_type=type)`
4. Only allowed override fields saved in `config` JSON field
5. On read, system merges: Global defaults → Account overrides
---
### 5. Unused/Deprecated Fields
| Field/Item | Location | Status |
|------------|----------|--------|
| `reference_id` | CreditTransaction model | **DEPRECATED** - Use `payment` FK instead |
| `mobile_image_size` | GlobalIntegrationSettings | **REMOVED** - No longer needed |
| `max_items` parameter | validators.py | **Deprecated** - No longer enforced |
| `get_model()` method | AICore class | **DEPRECATED** - Raises ValueError, model must be passed directly |
| `run_request()` method | AICore class | **DEPRECATED** - Redirects to `run_ai_request()` |
| `persist_task_metadata_to_content()` | MetadataMappingService | **DEPRECATED** - Content model no longer has task field |
| `DeploymentService` | publishing/services/ | **DEPRECATED** - Legacy SiteBlueprint service |
| SiteBlueprint model references | Multiple files | **REMOVED** - SiteBuilder deprecated |
---
### 6. Image Generation: Internal Cost vs Customer Credit Allocation
#### Internal Cost (What Platform Pays to Providers)
| Model | Provider | Cost Per Image (USD) |
|-------|----------|---------------------|
| dall-e-3 | OpenAI | $0.040 |
| dall-e-2 | OpenAI | $0.020 |
| gpt-image-1 | OpenAI | $0.042 |
| gpt-image-1-mini | OpenAI | $0.011 |
| runware:100@1 | Runware | ~$0.008-0.009 |
| bria-2.3 | Bria | ~$0.015 |
**Storage**: AIModelConfig.`cost_per_image` field + legacy `IMAGE_MODEL_RATES` constants
#### Customer Credit Cost (What Customer Pays)
| Operation | Credits Charged | Price per Credit | Min Charge |
|-----------|-----------------|------------------|------------|
| Image Generation | 5 credits | $0.02 | $0.10 |
| Image Prompt Extraction | 2 credits | $0.01 | $0.02 |
**Configuration Model**: `CreditCostConfig`
- `tokens_per_credit`: 50 (image gen uses fewer tokens per credit = higher cost)
- `min_credits`: 5
- `price_per_credit_usd`: $0.02
#### Margin Calculation
| Metric | DALL-E 3 Example |
|--------|------------------|
| Provider Cost | $0.040 |
| Customer Charge | $0.10 (5 credits × $0.02) |
| **Margin** | $0.06 (60% of customer charge) |
| **Markup** | ~150% |
**Flow**:
1. **Before AI call**: Calculate required credits based on image count
2. **Check balance**: Verify account has sufficient credits
3. **Deduct credits**: Remove from balance, log transaction
4. **Execute**: Make AI provider API call
5. **Track**: `CreditUsageLog` stores both `credits_used` (customer) and `cost_usd` (actual)
**Revenue Analytics** queries `CreditUsageLog` to calculate:
- Total revenue = Σ(credits_used × credit_price)
- Total cost = Σ(cost_usd from provider)
- Margin = Revenue - Cost
---
### Summary
The IGNY8 platform implements a sophisticated multi-tier configuration system:
- **AI configuration** is database-driven with fallback to legacy constants
- **Global settings** hold platform API keys; accounts only override model/parameters
- **User settings** create per-user records keyed by user + account + key combination
- **Credit system** charges customers a markup (~150%) over actual provider costs
- **Several fields are deprecated** including `mobile_image_size`, `reference_id`, and the `get_model()` method