- |
- {new Date(transaction.created_at).toLocaleDateString()}
- |
-
-
- {transaction.transaction_type}
-
- |
- = 0
- ? 'text-success-600 dark:text-success-400'
- : 'text-error-600 dark:text-error-400'
- }`}>
- {transaction.amount >= 0 ? '+' : ''}{transaction.amount.toLocaleString()}
+
+
+
+
+
+ | Date |
+ Type |
+ Amount |
+ Reference |
+ Description |
+
+
+
+ {transactions.map((transaction) => (
+
+ |
+ {new Date(transaction.created_at).toLocaleDateString()}
+ |
+
+
+ {transaction.transaction_type}
+
+ |
+ = 0
+ ? 'text-success-600 dark:text-success-400'
+ : 'text-error-600 dark:text-error-400'
+ }`}>
+ {transaction.amount >= 0 ? '+' : ''}{transaction.amount.toLocaleString()}
|
{transaction.reference_id || '-'}
@@ -101,8 +97,7 @@ export default function Transactions() {
|
- )}
-
+ >
);
}
diff --git a/frontend/src/pages/Billing/Usage.tsx b/frontend/src/pages/Billing/Usage.tsx
index 0585394c..589599c5 100644
--- a/frontend/src/pages/Billing/Usage.tsx
+++ b/frontend/src/pages/Billing/Usage.tsx
@@ -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 = {
@@ -20,9 +21,9 @@ const CREDIT_COSTS: Record([]);
const [balance, setBalance] = useState(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 (
-
- );
- }
-
return (
-
+ <>
Credit Usage & Activity
@@ -175,6 +165,6 @@ export default function Usage() {
-
+ >
);
}
diff --git a/frontend/src/pages/Blank.tsx b/frontend/src/pages/Blank.tsx
deleted file mode 100644
index 5567cd9d..00000000
--- a/frontend/src/pages/Blank.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import PageMeta from "../components/common/PageMeta";
-
-export default function Blank() {
- return (
-
-
-
-
-
- Card Title Here
-
-
-
- Start putting content on grids or panels, you can also use different
- combinations of grids.Please check out the dashboard and other pages
-
-
-
-
- );
-}
diff --git a/frontend/src/pages/Calendar.tsx b/frontend/src/pages/Calendar.tsx
deleted file mode 100644
index f3b7e35f..00000000
--- a/frontend/src/pages/Calendar.tsx
+++ /dev/null
@@ -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(
- null
- );
- const [eventTitle, setEventTitle] = useState("");
- const [eventStartDate, setEventStartDate] = useState("");
- const [eventEndDate, setEventEndDate] = useState("");
- const [eventLevel, setEventLevel] = useState("");
- const [events, setEvents] = useState([]);
- const calendarRef = useRef(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 (
- <>
-
-
-
-
-
-
-
-
-
- {selectedEvent ? "Edit Event" : "Add Event"}
-
-
- Plan your next big moment: schedule or edit an event to stay on
- track
-
-
-
-
-
-
- setEventTitle(e.target.value)}
- />
-
-
-
-
-
- {Object.entries(calendarsEvents).map(([key, value]) => (
-
-
-
-
-
- ))}
-
-
-
-
-
-
- setEventStartDate(e.target.value)}
- />
-
-
-
-
-
-
- setEventEndDate(e.target.value)}
- />
-
-
-
-
-
-
-
-
-
-
- >
- );
-};
-
-const renderEventContent = (eventInfo: any) => {
- const colorClass = `fc-bg-${eventInfo.event.extendedProps.calendar.toLowerCase()}`;
- return (
-
-
- {eventInfo.timeText}
- {eventInfo.event.title}
-
- );
-};
-
-export default Calendar;
diff --git a/frontend/src/pages/Charts/BarChart.tsx b/frontend/src/pages/Charts/BarChart.tsx
deleted file mode 100644
index f6babf5f..00000000
--- a/frontend/src/pages/Charts/BarChart.tsx
+++ /dev/null
@@ -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 (
-
- );
-}
diff --git a/frontend/src/pages/Charts/LineChart.tsx b/frontend/src/pages/Charts/LineChart.tsx
deleted file mode 100644
index d6619409..00000000
--- a/frontend/src/pages/Charts/LineChart.tsx
+++ /dev/null
@@ -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 (
- <>
-
-
-
-
-
-
- >
- );
-}
diff --git a/frontend/src/pages/Forms/FormElements.tsx b/frontend/src/pages/Forms/FormElements.tsx
deleted file mode 100644
index 8bac3731..00000000
--- a/frontend/src/pages/Forms/FormElements.tsx
+++ /dev/null
@@ -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 (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/frontend/src/pages/Linker/ContentList.tsx b/frontend/src/pages/Linker/ContentList.tsx
index 11a6e45b..abeca16f 100644
--- a/frontend/src/pages/Linker/ContentList.tsx
+++ b/frontend/src/pages/Linker/ContentList.tsx
@@ -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([]);
- const [loading, setLoading] = useState(true);
const [processing, setProcessing] = useState(null);
const [linkResults, setLinkResults] = useState>({});
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,24 +110,18 @@ export default function LinkerContentList() {
]} />}
/>
- {loading ? (
-
- ) : (
-
-
-
-
-
- |
- Title
- |
-
- Source
- |
-
+
+
+
+
+
+ |
+ Title
+ |
+
+ Source
+ |
+
Cluster
|
@@ -233,7 +228,6 @@ export default function LinkerContentList() {
)}
- )}
{/* Module footer placeholder - module on hold */}
diff --git a/frontend/src/pages/Linker/Dashboard.tsx b/frontend/src/pages/Linker/Dashboard.tsx
deleted file mode 100644
index 95fc0e7b..00000000
--- a/frontend/src/pages/Linker/Dashboard.tsx
+++ /dev/null
@@ -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(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 (
- <>
-
-
-
-
- ,
- color: 'blue',
- }}
- />
-
-
- View Content
-
-
-
- Manage internal linking for your content
-
-
- {loading ? (
-
- ) : stats ? (
- <>
- {/* Stats Cards */}
-
- }
- accentColor="blue"
- onClick={() => navigate('/linker/content')}
- />
-
- }
- accentColor="purple"
- onClick={() => navigate('/linker/content')}
- />
-
- }
- accentColor="green"
- onClick={() => navigate('/linker/content')}
- />
-
-
- {/* Quick Actions */}
-
-
-
-
-
-
- Link Content
- Process content for internal linking
-
-
-
-
-
-
-
-
-
- View Content
- Browse all content items
-
-
-
-
-
-
- >
- ) : (
-
- )}
-
- >
- );
-}
-
diff --git a/frontend/src/pages/Optimizer/Dashboard.tsx b/frontend/src/pages/Optimizer/Dashboard.tsx
deleted file mode 100644
index 6dc48917..00000000
--- a/frontend/src/pages/Optimizer/Dashboard.tsx
+++ /dev/null
@@ -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(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 (
- <>
-
-
-
-
- ,
- color: 'orange',
- }}
- />
-
-
- Optimize Content
-
-
-
- Optimize your content for SEO, readability, and engagement
-
-
- {loading ? (
-
- ) : stats ? (
- <>
- {/* Stats Cards */}
-
- }
- accentColor="blue"
- onClick={() => navigate('/optimizer/content')}
- />
-
- }
- accentColor="green"
- onClick={() => navigate('/optimizer/content')}
- />
-
- }
- accentColor="orange"
- onClick={() => navigate('/optimizer/content')}
- />
-
-
- {/* Quick Actions */}
-
-
-
-
-
-
- Optimize Content
- Select and optimize content items
-
-
-
-
-
-
-
-
-
- View Content
- Browse all content items
-
-
-
-
-
-
- >
- ) : (
-
- )}
-
- >
- );
-}
-
diff --git a/frontend/src/pages/Planner/ClusterDetail.tsx b/frontend/src/pages/Planner/ClusterDetail.tsx
index f22de8be..ef2b9167 100644
--- a/frontend/src/pages/Planner/ClusterDetail.tsx
+++ b/frontend/src/pages/Planner/ClusterDetail.tsx
@@ -121,18 +121,18 @@ export default function ClusterDetail() {
if (loading) {
return (
-
+ >
);
}
if (!cluster) {
return (
-
+ <>
Cluster not found
@@ -140,12 +140,12 @@ export default function ClusterDetail() {
Back to Clusters
-
+ >
);
}
return (
-
+ >
);
}
diff --git a/frontend/src/pages/Planner/Dashboard.tsx b/frontend/src/pages/Planner/Dashboard.tsx
deleted file mode 100644
index 589ff384..00000000
--- a/frontend/src/pages/Planner/Dashboard.tsx
+++ /dev/null
@@ -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;
- byCountry: Record;
- };
- 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;
- byContentType: Record;
- };
- 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(null);
- const [loading, setLoading] = useState(true);
- const [lastUpdated, setLastUpdated] = useState(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 = {};
- const keywordsByCountry: Record = {};
- 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 = {};
- const ideasByContentType: Record = {};
- 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 (
- <>
-
-
-
-
- Loading dashboard data...
-
-
- >
- );
- }
-
- if (!stats && !loading) {
- return (
- <>
-
-
-
- {activeSector ? 'No data available for the selected sector.' : 'No data available. Select a sector or wait for data to load.'}
-
-
- >
- );
- }
-
- if (!stats) return null;
-
- return (
- <>
-
-
-
-
- {/* Key Metrics */}
-
- }
- accentColor="blue"
- trend={0}
- href="/planner/keywords"
- />
- }
- accentColor="green"
- trend={0}
- href="/planner/clusters"
- />
- }
- accentColor="orange"
- trend={0}
- href="/planner/ideas"
- />
- }
- accentColor="purple"
- trend={0}
- href="/planner/keywords"
- />
-
-
- {/* Planner Modules */}
-
-
- {plannerModules.map((module) => {
- const Icon = module.icon;
- return (
-
-
- {module.title}
- {module.description}
-
-
- {module.count}
- {module.metric}
-
-
-
-
- );
- })}
-
-
-
- {/* Activity Chart & Recent Activity */}
-
-
- Loading chart... }>
-
-
-
-
-
-
- {recentActivity.map((activity) => {
- const Icon = activity.icon;
- return (
-
-
-
-
-
-
- {activity.type}
- {formatTimeAgo(activity.timestamp)}
-
- {activity.description}
-
-
- );
- })}
-
-
-
-
- {/* Charts */}
-
- {keywordsStatusChart && (
-
- Loading chart... }>
-
-
-
- )}
-
- {topClustersChart && (
-
- Loading chart...}>
-
-
-
- )}
-
-
- {/* Progress Summary */}
-
-
-
-
- Keyword Mapping
- {keywordMappingPct}%
-
-
-
- {stats.keywords.mapped} of {stats.keywords.total} keywords mapped
-
-
-
-
-
- Clusters With Ideas
- {clustersIdeasPct}%
-
-
-
- {stats.clusters.withIdeas} of {stats.clusters.total} clusters have ideas
-
-
-
-
-
- Ideas Queued to Writer
- {ideasQueuedPct}%
-
-
-
- {stats.ideas.queued} of {stats.ideas.total} ideas queued
-
-
-
-
-
- {/* Quick Actions */}
-
-
-
-
-
-
-
- Add Keywords
- Discover opportunities
-
-
-
-
-
-
-
-
-
- Auto Cluster
- Group keywords
-
-
-
-
-
-
-
-
-
- Generate Ideas
- Create content ideas
-
-
-
-
-
-
-
- Setup Automation
- Automate workflows
-
-
-
-
-
-
- {/* Info Cards */}
-
-
-
-
-
-
-
-
- Keyword Discovery
-
- Discover high-volume keywords from our global database. Add keywords manually or import from keyword opportunities.
-
-
-
-
-
-
-
-
- AI Clustering
-
- Automatically group related keywords into strategic clusters. Each cluster represents a content topic with shared search intent.
-
-
-
-
-
-
-
-
- Idea Generation
-
- Generate content ideas from clusters using AI. Each idea includes title, outline, and target keywords for content creation.
-
-
-
-
-
-
-
-
-
-
- 1
-
-
- Add Keywords
-
- Start by adding keywords from the keyword opportunities page. You can search by volume, difficulty, or intent.
-
-
-
-
-
- 2
-
-
- Cluster Keywords
-
- Use the auto-cluster feature to group related keywords. Review and refine clusters to match your content strategy.
-
-
-
-
-
- 3
-
-
- Generate Ideas
-
- Create content ideas from your clusters. Queue ideas to the Writer module to start content creation.
-
-
-
-
-
-
-
- >
- );
-}
diff --git a/frontend/src/pages/Planner/Mapping.tsx b/frontend/src/pages/Planner/Mapping.tsx
deleted file mode 100644
index 2688f859..00000000
--- a/frontend/src/pages/Planner/Mapping.tsx
+++ /dev/null
@@ -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 (
- <>
-
- , color: 'indigo' }}
- />
-
-
-
- Content Mapping - Coming Soon
-
-
- Map keywords and clusters to existing pages and content
-
-
-
- >
- );
-}
-
diff --git a/frontend/src/pages/Publisher/ContentCalendar.tsx b/frontend/src/pages/Publisher/ContentCalendar.tsx
index cf54c59e..287831b1 100644
--- a/frontend/src/pages/Publisher/ContentCalendar.tsx
+++ b/frontend/src/pages/Publisher/ContentCalendar.tsx
@@ -382,29 +382,29 @@ export default function ContentCalendar() {
if (!activeSite) {
return (
-
+ <>
Please select a site from the header to view the content calendar
-
+ >
);
}
if (loading) {
return (
-
+ >
);
}
return (
-
-
+ >
);
}
diff --git a/frontend/src/pages/Reference/Industries.tsx b/frontend/src/pages/Reference/Industries.tsx
index 46a1928d..e4829088 100644
--- a/frontend/src/pages/Reference/Industries.tsx
+++ b/frontend/src/pages/Reference/Industries.tsx
@@ -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([]);
- 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,25 +99,19 @@ export default function Industries() {
badge={{ icon: , color: 'blue' }}
hideSiteSector={true}
/>
-
-
-
- Explore our comprehensive global database of industries, sectors, and high-volume keywords
-
-
+
+
+ Explore our comprehensive global database of industries, sectors, and high-volume keywords
+
+
- {loading ? (
-
- Loading industries...
-
- ) : (
-
- {industries.map((industry) => (
-
- {/* Header */}
+
+ {industries.map((industry) => (
+
+ {/* Header */}
{industry.name}
@@ -183,8 +178,6 @@ export default function Industries() {
)}
))}
-
- )}
>
);
diff --git a/frontend/src/pages/Reference/SeedKeywords.tsx b/frontend/src/pages/Reference/SeedKeywords.tsx
index d1e9c5da..10000ed5 100644
--- a/frontend/src/pages/Reference/SeedKeywords.tsx
+++ b/frontend/src/pages/Reference/SeedKeywords.tsx
@@ -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([]);
const [industries, setIndustries] = useState([]);
- const [loading, setLoading] = useState(true);
const [selectedIndustry, setSelectedIndustry] = useState(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 (
-
+ <>
Seed Keywords
@@ -72,32 +73,27 @@ export default function SeedKeywords() {
/>
- {loading ? (
-
- ) : (
-
-
-
-
-
- | Keyword |
- Industry |
- Sector |
- Volume |
- Difficulty |
- Country |
-
-
-
- {keywords.map((keyword) => (
-
- |
- {keyword.keyword}
- |
-
- {keyword.industry_name}
+
+
+
+
+
+ | Keyword |
+ Industry |
+ Sector |
+ Volume |
+ Difficulty |
+ Country |
+
+
+
+ {keywords.map((keyword) => (
+
+ |
+ {keyword.keyword}
+ |
+
+ {keyword.industry_name}
|
{keyword.sector_name}
@@ -118,7 +114,7 @@ export default function SeedKeywords() {
)}
-
+ >
);
}
diff --git a/frontend/src/pages/Schedules.tsx b/frontend/src/pages/Schedules.tsx
deleted file mode 100644
index 47ee8076..00000000
--- a/frontend/src/pages/Schedules.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import PageMeta from "../components/common/PageMeta";
-import ComponentCard from "../components/common/ComponentCard";
-
-export default function Schedules() {
- return (
- <>
-
-
-
-
-
- Schedules - Coming Soon
-
-
- Content scheduling and automation for consistent publishing
-
-
-
- >
- );
-}
-
diff --git a/frontend/src/pages/Settings/Account.tsx b/frontend/src/pages/Settings/Account.tsx
index 541ba7c8..2d575d8e 100644
--- a/frontend/src/pages/Settings/Account.tsx
+++ b/frontend/src/pages/Settings/Account.tsx
@@ -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([]);
- 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 (
-
+ <>
Account Settings
Manage your account preferences and profile
- {loading ? (
-
- ) : (
-
- Account settings management interface coming soon.
-
- )}
-
+
+ Account settings management interface coming soon.
+
+ >
);
}
diff --git a/frontend/src/pages/Settings/CreditsAndBilling.tsx b/frontend/src/pages/Settings/CreditsAndBilling.tsx
index 87c423c8..7ad2a238 100644
--- a/frontend/src/pages/Settings/CreditsAndBilling.tsx
+++ b/frontend/src/pages/Settings/CreditsAndBilling.tsx
@@ -66,18 +66,18 @@ const CreditsAndBilling: React.FC = () => {
if (loading) {
return (
-
+ >
);
}
return (
-
+ <>
@@ -243,7 +243,7 @@ const CreditsAndBilling: React.FC = () => {
)}
-
+ >
);
};
diff --git a/frontend/src/pages/Settings/ImportExport.tsx b/frontend/src/pages/Settings/ImportExport.tsx
deleted file mode 100644
index 662f7852..00000000
--- a/frontend/src/pages/Settings/ImportExport.tsx
+++ /dev/null
@@ -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 (
- <>
-
-
-
-
-
-
-
- Coming Soon: Manage Your Data
-
-
-
- Import and Export Your Content - Backup your keywords, articles, and settings. Move your content to other platforms. Download everything safely.
-
-
-
-
- What will be available:
-
-
-
-
-
- Export your keywords as a file (backup or share)
-
-
-
-
-
- Export all your articles in different formats
-
-
-
-
-
- Import keywords from other sources
-
-
-
-
-
- Backup and restore your entire account
-
-
-
-
-
- Download your settings and configurations
-
-
-
-
-
-
- >
- );
-}
-
diff --git a/frontend/src/pages/Settings/Industries.tsx b/frontend/src/pages/Settings/Industries.tsx
index 155bb87b..62482600 100644
--- a/frontend/src/pages/Settings/Industries.tsx
+++ b/frontend/src/pages/Settings/Industries.tsx
@@ -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([]);
- const [loading, setLoading] = useState(true);
useEffect(() => {
loadIndustries();
@@ -16,49 +17,43 @@ 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 (
-
+ <>
Industries
Manage global industry templates (Admin Only)
- {loading ? (
-
- ) : (
-
- {industries.map((industry) => (
-
-
- {industry.name}
-
- {industry.is_active ? 'Active' : 'Inactive'}
-
-
- {industry.description && (
- {industry.description}
- )}
-
- Sectors: {industry.sectors_count || 0}
-
-
- ))}
-
- )}
-
+
+ {industries.map((industry) => (
+
+
+ {industry.name}
+
+ {industry.is_active ? 'Active' : 'Inactive'}
+
+
+ {industry.description && (
+ {industry.description}
+ )}
+
+ Sectors: {industry.sectors_count || 0}
+
+
+ ))}
+
+ >
);
}
diff --git a/frontend/src/pages/Settings/Plans.tsx b/frontend/src/pages/Settings/Plans.tsx
index a02a7311..0545e6d5 100644
--- a/frontend/src/pages/Settings/Plans.tsx
+++ b/frontend/src/pages/Settings/Plans.tsx
@@ -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([]);
- 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 (
-
+ <>
Plans
@@ -157,11 +158,7 @@ export default function Plans() {
- {loading ? (
-
- ) : pricingPlans.length === 0 ? (
+ {pricingPlans.length === 0 ? (
No active plans available
@@ -183,6 +180,6 @@ export default function Plans() {
>
)}
-
+ >
);
}
diff --git a/frontend/src/pages/Settings/Publishing.tsx b/frontend/src/pages/Settings/Publishing.tsx
index 5064dd9d..be8eac8d 100644
--- a/frontend/src/pages/Settings/Publishing.tsx
+++ b/frontend/src/pages/Settings/Publishing.tsx
@@ -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(['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 (
-
- );
- }
-
return (
-
+ <>
@@ -230,7 +220,7 @@ export default function Publishing() {
-
+ >
);
}
diff --git a/frontend/src/pages/Settings/Status.tsx b/frontend/src/pages/Settings/Status.tsx
deleted file mode 100644
index 04eb0c6c..00000000
--- a/frontend/src/pages/Settings/Status.tsx
+++ /dev/null
@@ -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(null);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(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 (
- <>
-
-
-
-
- >
- );
- }
-
- if (error || !status) {
- return (
- <>
-
-
-
- {error || 'Failed to load system status'}
-
-
- >
- );
- }
-
- return (
- <>
-
-
-
- {/* System Resources */}
-
-
- {/* CPU */}
-
-
- CPU
-
- {status.system?.cpu?.status || 'unknown'}
-
-
-
-
- {status.system?.cpu?.usage_percent?.toFixed(1)}% used ({status.system?.cpu?.cores} cores)
-
-
-
- {/* Memory */}
-
-
- Memory
-
- {status.system?.memory?.status || 'unknown'}
-
-
-
-
- {status.system?.memory?.used_gb?.toFixed(1)} GB / {status.system?.memory?.total_gb?.toFixed(1)} GB
-
-
-
- {/* Disk */}
-
-
- Disk
-
- {status.system?.disk?.status || 'unknown'}
-
-
-
-
- {status.system?.disk?.used_gb?.toFixed(1)} GB / {status.system?.disk?.total_gb?.toFixed(1)} GB
-
-
-
-
-
- {/* Services Status */}
-
- {/* Database */}
-
-
-
- Status
-
- {status.database?.connected ? 'Connected' : 'Disconnected'}
-
-
- {status.database?.version && (
-
- Version:
- {status.database.version.split(',')[0]}
-
- )}
- {status.database?.size && (
-
- Size:
- {status.database.size}
-
- )}
-
- Active Connections:
- {status.database?.active_connections || 0}
-
-
-
-
- {/* Redis */}
-
-
-
- Status
-
- {status.redis?.connected ? 'Connected' : 'Disconnected'}
-
-
-
-
-
- {/* Celery */}
-
-
-
- Workers
-
- {status.celery?.worker_count || 0} active
-
-
-
- Active Tasks:
- {status.celery?.tasks?.active || 0}
-
-
- Scheduled:
- {status.celery?.tasks?.scheduled || 0}
-
-
- Reserved:
- {status.celery?.tasks?.reserved || 0}
-
-
-
-
-
- {/* Process Monitoring by Stack */}
-
-
-
-
-
- | Stack |
- Processes |
- CPU % |
- Memory (MB) |
-
-
-
- {Object.entries(status.processes?.by_stack || {}).map(([stack, stats]) => (
-
- | {stack} |
- {stats.count} |
- {stats.cpu.toFixed(2)}% |
- {stats.memory_mb.toFixed(2)} |
-
- ))}
-
-
-
-
-
- {/* Module Statistics */}
-
-
- {/* Planner Module */}
-
- Planner Module
-
-
- Keywords:
- {status.modules?.planner?.keywords?.toLocaleString() || 0}
-
-
- Clusters:
- {status.modules?.planner?.clusters?.toLocaleString() || 0}
-
-
- Content Ideas:
- {status.modules?.planner?.content_ideas?.toLocaleString() || 0}
-
-
-
-
- {/* Writer Module */}
-
- Writer Module
-
-
- Tasks:
- {status.modules?.writer?.tasks?.toLocaleString() || 0}
-
-
- Images:
- {status.modules?.writer?.images?.toLocaleString() || 0}
-
-
-
-
-
-
- {/* Last Updated */}
-
- Last updated: {new Date(status.timestamp).toLocaleString()}
-
-
- >
- );
-}
diff --git a/frontend/src/pages/Settings/Subscriptions.tsx b/frontend/src/pages/Settings/Subscriptions.tsx
index f8b6b046..2258bbea 100644
--- a/frontend/src/pages/Settings/Subscriptions.tsx
+++ b/frontend/src/pages/Settings/Subscriptions.tsx
@@ -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([]);
- 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,52 +49,46 @@ export default function Subscriptions() {
};
return (
-
+ <>
Subscriptions
Manage account subscriptions
- {loading ? (
-
- ) : (
-
-
-
-
-
- | Account |
- Status |
- Period Start |
- Period End |
+
+
+
+
+
+ | Account |
+ Status |
+ Period Start |
+ Period End |
+
+
+
+ {subscriptions.map((subscription) => (
+
+ | {subscription.account_name} |
+
+
+ {subscription.status}
+
+ |
+
+ {new Date(subscription.current_period_start).toLocaleDateString()}
+ |
+
+ {new Date(subscription.current_period_end).toLocaleDateString()}
+ |
-
-
- {subscriptions.map((subscription) => (
-
- | {subscription.account_name} |
-
-
- {subscription.status}
-
- |
-
- {new Date(subscription.current_period_start).toLocaleDateString()}
- |
-
- {new Date(subscription.current_period_end).toLocaleDateString()}
- |
-
- ))}
-
-
-
-
- )}
-
+ ))}
+
+
+
+
+ >
);
}
diff --git a/frontend/src/pages/Settings/System.tsx b/frontend/src/pages/Settings/System.tsx
index 1254a55f..13f051d6 100644
--- a/frontend/src/pages/Settings/System.tsx
+++ b/frontend/src/pages/Settings/System.tsx
@@ -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 ([]);
- 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 (
-
+ <>
System Settings
Global platform-wide settings
- {loading ? (
-
- ) : (
-
- System settings management interface coming soon.
-
- )}
-
+
+ System settings management interface coming soon.
+
+ >
);
}
diff --git a/frontend/src/pages/Settings/Users.tsx b/frontend/src/pages/Settings/Users.tsx
index 01aebc1e..9f5da97c 100644
--- a/frontend/src/pages/Settings/Users.tsx
+++ b/frontend/src/pages/Settings/Users.tsx
@@ -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([]);
- const [loading, setLoading] = useState(true);
useEffect(() => {
loadUsers();
@@ -24,61 +25,55 @@ 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 (
-
+ <>
Users
Manage account users and permissions
- {loading ? (
-
- ) : (
-
-
-
-
-
- | Email |
- Username |
- Role |
- Status |
+
+
+
+
+
+ | Email |
+ Username |
+ Role |
+ Status |
+
+
+
+ {users.map((user) => (
+
+ | {user.email} |
+ {user.username} |
+
+ {user.role}
+ |
+
+
+ {user.is_active ? 'Active' : 'Inactive'}
+
+ |
-
-
- {users.map((user) => (
-
- | {user.email} |
- {user.username} |
-
- {user.role}
- |
-
-
- {user.is_active ? 'Active' : 'Inactive'}
-
- |
-
- ))}
-
-
-
-
- )}
-
+ ))}
+
+
+
+
+ >
);
}
diff --git a/frontend/src/pages/Setup/IndustriesSectorsKeywords.tsx b/frontend/src/pages/Setup/IndustriesSectorsKeywords.tsx
index 4b9147de..3bf2da0b 100644
--- a/frontend/src/pages/Setup/IndustriesSectorsKeywords.tsx
+++ b/frontend/src/pages/Setup/IndustriesSectorsKeywords.tsx
@@ -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 ([]);
const [seedKeywords, setSeedKeywords] = useState<(SeedKeyword & { isAdded?: boolean })[]>([]);
- const [loading, setLoading] = useState(true);
const [showContent, setShowContent] = useState(false);
const [selectedIds, setSelectedIds] = useState([]);
// 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 (
- <>
-
- , color: 'blue' }}
- />
-
- >
- );
- }
-
// Show WorkflowGuide if no sites
if (sites.length === 0) {
return (
diff --git a/frontend/src/pages/Sites/Content.tsx b/frontend/src/pages/Sites/Content.tsx
index c9a8c1ba..5ee736aa 100644
--- a/frontend/src/pages/Sites/Content.tsx
+++ b/frontend/src/pages/Sites/Content.tsx
@@ -150,7 +150,7 @@ export default function SiteContentManager() {
];
return (
-
+ >
);
}
diff --git a/frontend/src/pages/Sites/Dashboard.tsx b/frontend/src/pages/Sites/Dashboard.tsx
index 30184bec..872489fd 100644
--- a/frontend/src/pages/Sites/Dashboard.tsx
+++ b/frontend/src/pages/Sites/Dashboard.tsx
@@ -231,18 +231,18 @@ export default function SiteDashboard() {
if (loading) {
return (
-
+ <>
Loading site dashboard...
-
+ >
);
}
if (!site) {
return (
-
+ <>
Site not found
@@ -250,12 +250,12 @@ export default function SiteDashboard() {
Back to Sites
-
+ >
);
}
return (
-
+ >
);
}
diff --git a/frontend/src/pages/Sites/Editor.tsx b/frontend/src/pages/Sites/Editor.tsx
deleted file mode 100644
index a0a05c5d..00000000
--- a/frontend/src/pages/Sites/Editor.tsx
+++ /dev/null
@@ -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 (
-
-
-
-
-
-
- Feature Deprecated
-
- The SiteBlueprint page editor has been removed.
- Please use the Writer module to create and edit content.
-
-
-
-
- );
-}
diff --git a/frontend/src/pages/Sites/List.tsx b/frontend/src/pages/Sites/List.tsx
index 25b8f3c8..aef4b12d 100644
--- a/frontend/src/pages/Sites/List.tsx
+++ b/frontend/src/pages/Sites/List.tsx
@@ -479,17 +479,17 @@ export default function SiteList() {
if (loading) {
return (
-
+ >
);
}
return (
-
+ >
);
}
diff --git a/frontend/src/pages/Sites/PageManager.tsx b/frontend/src/pages/Sites/PageManager.tsx
index 12c9125f..8542d5f2 100644
--- a/frontend/src/pages/Sites/PageManager.tsx
+++ b/frontend/src/pages/Sites/PageManager.tsx
@@ -266,17 +266,17 @@ export default function PageManager() {
if (loading) {
return (
-
+ >
);
}
return (
-
+ >
);
}
diff --git a/frontend/src/pages/Sites/PostEditor.tsx b/frontend/src/pages/Sites/PostEditor.tsx
index 2108ec5a..3929a36a 100644
--- a/frontend/src/pages/Sites/PostEditor.tsx
+++ b/frontend/src/pages/Sites/PostEditor.tsx
@@ -218,17 +218,17 @@ export default function PostEditor() {
if (loading) {
return (
-
+ >
);
}
return (
-
+ <>
@@ -677,7 +677,7 @@ export default function PostEditor() {
)}
-
+ >
);
}
diff --git a/frontend/src/pages/Sites/PublishingQueue.tsx b/frontend/src/pages/Sites/PublishingQueue.tsx
deleted file mode 100644
index 133fa849..00000000
--- a/frontend/src/pages/Sites/PublishingQueue.tsx
+++ /dev/null
@@ -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([]);
- const [viewMode, setViewMode] = useState('list');
- const [draggedItem, setDraggedItem] = useState(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 (
-
-
- Paused
-
- );
- }
- if (item.site_status === 'publishing') {
- return (
-
-
- Publishing...
-
- );
- }
- return (
-
-
- Scheduled
-
- );
- };
-
- // 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 (
-
- );
- }
-
- return (
-
-
- , color: 'amber' }}
- breadcrumb="Sites / Publishing Queue"
- />
-
- {/* Stats Overview */}
-
-
-
-
-
-
-
- {stats.scheduled}
- Scheduled
-
-
-
-
-
-
-
- {stats.publishing}
- Publishing
-
-
-
-
-
-
-
-
-
- {stats.published}
- Published
-
-
-
-
-
-
-
-
-
- {stats.failed}
- Failed
-
-
-
-
-
- {/* View Toggle */}
-
-
- {queueItems.length} items in queue
-
-
- setViewMode('list')}
- startIcon={}
- >
- List
-
- setViewMode('calendar')}
- startIcon={}
- >
- Calendar
-
-
-
-
- {/* Queue Content */}
- {queueItems.length === 0 ? (
-
-
-
- No content scheduled
-
-
- Content will appear here when it's scheduled for publishing.
-
-
-
- ) : viewMode === 'list' ? (
- /* List View */
-
-
- {queueItems.map((item, index) => (
- 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 */}
-
- {index + 1}
-
-
- {/* Content info */}
-
-
- {item.title}
-
-
-
-
- {formatScheduledTime(item.scheduled_publish_at)}
-
- •
- {item.content_type}
-
-
-
- {/* Status badge */}
- {getStatusBadge(item)}
-
- {/* Actions */}
-
- }
- variant="ghost"
- tone="neutral"
- size="sm"
- onClick={() => handleViewContent(item)}
- title="View content"
- />
- : }
- variant="ghost"
- tone="neutral"
- size="sm"
- onClick={() => handlePauseItem(item)}
- title={item.isPaused ? 'Resume' : 'Pause'}
- />
- }
- variant="ghost"
- tone="danger"
- size="sm"
- onClick={() => handleRemoveFromQueue(item)}
- title="Remove from queue"
- />
-
-
- ))}
-
-
- ) : (
- /* Calendar View */
-
-
- {/* Day headers */}
- {['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(day => (
-
- {day}
-
- ))}
-
- {/* Calendar days */}
- {getCalendarDays().map((date, index) => {
- const dayItems = getItemsForDate(date);
- const isToday = date.toDateString() === new Date().toDateString();
-
- return (
-
-
- {date.getDate()}
-
-
- {dayItems.slice(0, 3).map(item => (
- 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}
-
- ))}
- {dayItems.length > 3 && (
-
- +{dayItems.length - 3} more
-
- )}
-
-
- );
- })}
-
-
- )}
-
- {/* Actions */}
-
-
-
-
-
- );
-}
diff --git a/frontend/src/pages/Sites/Settings.tsx b/frontend/src/pages/Sites/Settings.tsx
index 80338232..46bdee9d 100644
--- a/frontend/src/pages/Sites/Settings.tsx
+++ b/frontend/src/pages/Sites/Settings.tsx
@@ -669,17 +669,17 @@ export default function SiteSettings() {
if (loading) {
return (
-
+ >
);
}
return (
-
+ >
);
}
diff --git a/frontend/src/pages/Sites/SyncDashboard.tsx b/frontend/src/pages/Sites/SyncDashboard.tsx
index 45980bec..bfb68c14 100644
--- a/frontend/src/pages/Sites/SyncDashboard.tsx
+++ b/frontend/src/pages/Sites/SyncDashboard.tsx
@@ -118,18 +118,18 @@ export default function SyncDashboard() {
if (loading) {
return (
-
+ >
);
}
if (!syncStatus) {
return (
-
+ <>
@@ -137,7 +137,7 @@ export default function SyncDashboard() {
No sync data available
-
+ >
);
}
@@ -151,7 +151,7 @@ export default function SyncDashboard() {
(mismatches?.posts.missing_in_igny8.length || 0);
return (
-
+ >
);
}
diff --git a/frontend/src/pages/Tables/BasicTables.tsx b/frontend/src/pages/Tables/BasicTables.tsx
deleted file mode 100644
index aa940c82..00000000
--- a/frontend/src/pages/Tables/BasicTables.tsx
+++ /dev/null
@@ -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 (
- <>
-
-
-
-
-
-
- >
- );
-}
diff --git a/frontend/src/pages/Thinker/AuthorProfiles.tsx b/frontend/src/pages/Thinker/AuthorProfiles.tsx
index 46a2d63d..048670a9 100644
--- a/frontend/src/pages/Thinker/AuthorProfiles.tsx
+++ b/frontend/src/pages/Thinker/AuthorProfiles.tsx
@@ -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([]);
- const [loading, setLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingProfile, setEditingProfile] = useState(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 (
-
- {loading ? (
-
- ) : (
-
- {profiles.map((profile) => (
-
-
- {profile.name}
-
- {profile.is_active ? 'Active' : 'Inactive'}
-
+
+ {profiles.map((profile) => (
+
+
+ {profile.name}
+
+ {profile.is_active ? 'Active' : 'Inactive'}
+
+
+ {profile.description}
+
+
+ Tone:{' '}
+ {profile.tone}
- {profile.description}
-
-
- Tone:{' '}
- {profile.tone}
-
-
- Language:{' '}
- {profile.language}
-
+
+ Language:{' '}
+ {profile.language}
-
-
-
-
-
- ))}
-
- )}
+
+
+
+
+
+
+ ))}
+
-
+ >
);
}
diff --git a/frontend/src/pages/Thinker/Dashboard.tsx b/frontend/src/pages/Thinker/Dashboard.tsx
deleted file mode 100644
index a8c88a59..00000000
--- a/frontend/src/pages/Thinker/Dashboard.tsx
+++ /dev/null
@@ -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(null);
- const [loading, setLoading] = useState(true);
- const [lastUpdated, setLastUpdated] = useState(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 (
- <>
-
-
-
-
- {/* Key Metrics */}
-
- }
- trend={0}
- accentColor="orange"
- />
- }
- trend={0}
- accentColor="blue"
- />
- }
- trend={0}
- accentColor="purple"
- />
- }
- trend={0}
- accentColor="green"
- />
-
-
- {/* Thinker Modules */}
-
-
- {thinkerModules.map((module) => {
- const Icon = module.icon;
- return (
-
-
-
-
-
- {module.status === "coming-soon" && (
-
- Soon
-
- )}
-
- {module.title}
- {module.description}
- {module.count > 0 && (
-
- )}
- {module.status === "coming-soon" && (
-
- Coming soon
-
- )}
-
- );
- })}
-
-
-
- {/* Recent Activity & Usage Chart */}
-
-
- Loading chart... }>
-
-
-
-
-
-
- {recentPrompts.map((prompt) => (
-
-
-
-
-
-
- {prompt.name}
- {prompt.usage} uses
-
-
- {prompt.category}
- •
- {prompt.lastUsed}
-
-
-
- ))}
-
-
-
-
- {/* Quick Actions */}
-
-
-
-
-
-
-
-
-
-
- {/* Info Cards */}
-
-
-
-
-
-
-
-
- Centralized Control
-
- Manage all AI prompts, author voices, and brand guidelines in one place. Changes sync automatically to all content generation.
-
-
-
-
-
-
-
-
- Version Control
-
- Track changes to prompts and strategies with full version history. Roll back to previous versions when needed.
-
-
-
-
-
-
-
-
- Automated Enforcement
-
- Every piece of content automatically uses your defined prompts, author profiles, and brand guidelines.
-
-
-
-
-
-
-
-
-
-
- 1
-
-
- Create Author Profiles
-
- Define writing voices and styles that match your brand. Each profile can have unique tone, structure, and guidelines.
-
-
-
-
-
- 2
-
-
- Build Prompt Library
-
- Create reusable prompt templates for different content types. Use variables to make prompts dynamic and flexible.
-
-
-
-
-
- 3
-
-
- Define Strategies
-
- Create content playbooks that combine prompts, profiles, and guidelines. Apply strategies to specific clusters or content types.
-
-
-
-
-
-
-
- >
- );
-}
diff --git a/frontend/src/pages/UserProfiles.tsx b/frontend/src/pages/UserProfiles.tsx
deleted file mode 100644
index cf52b5bf..00000000
--- a/frontend/src/pages/UserProfiles.tsx
+++ /dev/null
@@ -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 (
- <>
-
-
-
- Profile
-
-
-
-
-
-
-
- >
- );
-}
diff --git a/frontend/src/pages/Writer/Dashboard.tsx b/frontend/src/pages/Writer/Dashboard.tsx
deleted file mode 100644
index f3640e0e..00000000
--- a/frontend/src/pages/Writer/Dashboard.tsx
+++ /dev/null
@@ -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;
- 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;
- };
- images: {
- total: number;
- generated: number;
- pending: number;
- failed: number;
- byType: Record;
- };
- 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(null);
- const [loading, setLoading] = useState(true);
- const [lastUpdated, setLastUpdated] = useState(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 = {};
- 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 = {};
-
- 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 = {};
-
- 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 (
- <>
-
-
-
-
- Loading dashboard data...
-
-
- >
- );
- }
-
- if (!stats && !loading) {
- return (
- <>
-
-
-
- {activeSector ? 'No data available for the selected sector.' : 'No data available. Select a sector or wait for data to load.'}
-
-
- >
- );
- }
-
- if (!stats) return null;
-
- return (
- <>
-
-
-
-
- {/* Key Metrics */}
-
- }
- accentColor="blue"
- trend={0}
- href="/writer/tasks"
- />
- }
- accentColor="green"
- trend={0}
- href="/writer/content"
- />
- }
- accentColor="orange"
- trend={0}
- href="/writer/images"
- />
- }
- accentColor="purple"
- trend={0}
- href="/writer/published"
- />
-
-
- {/* Writer Modules */}
-
-
- {writerModules.map((module) => {
- const Icon = module.icon;
- return (
-
-
- {module.title}
- {module.description}
-
-
- {module.count}
- {module.metric}
-
-
-
-
- );
- })}
-
-
-
- {/* Activity Chart & Recent Activity */}
-
-
- Loading chart... }>
-
-
-
-
-
-
- {recentActivity.map((activity) => {
- const Icon = activity.icon;
- return (
-
-
-
-
-
-
- {activity.type}
- {formatTimeAgo(activity.timestamp)}
-
- {activity.description}
-
-
- );
- })}
-
-
-
-
- {/* Charts */}
-
- {tasksStatusChart && (
-
- Loading chart... }>
-
-
-
- )}
-
- {contentStatusChart && (
-
- Loading chart... }>
-
-
-
- )}
-
-
- {/* Productivity Metrics */}
-
-
-
-
- Task Completion
- {completionRate}%
-
-
-
- {stats.tasks.completed} of {stats.tasks.total} tasks completed
-
-
-
-
-
- Publish Rate
- {stats.productivity.publishRate}%
-
-
-
- {stats.content.published} of {stats.content.total} content published
-
-
-
-
-
- Image Generation
-
- {stats.images.total > 0 ? Math.round((stats.images.generated / stats.images.total) * 100) : 0}%
-
-
- 0 ? Math.round((stats.images.generated / stats.images.total) * 100) : 0}
- color="warning"
- size="md"
- />
-
- {stats.images.generated} of {stats.images.total} images generated
-
-
-
-
-
- {/* Quick Actions */}
-
-
-
-
-
-
-
- Create Task
- New writing task
-
-
-
-
-
-
-
- Generate Content
- AI content creation
-
-
-
-
-
-
-
-
-
- Generate Images
- Create visuals
-
-
-
-
-
-
-
- Publish Content
- Publish to Site
-
-
-
-
-
-
- {/* Info Cards */}
-
-
-
-
-
-
-
-
- Task Creation
-
- Create writing tasks from content ideas. Each task includes target keywords, outline, and word count requirements.
-
-
-
-
-
-
- AI Content Generation
-
- Generate full content pieces using AI. Content is created based on your prompts, author profiles, and brand guidelines.
-
-
-
-
-
-
-
-
- Image Generation
-
- Automatically generate featured images and in-article images for your content. Images are optimized for SEO and engagement.
-
-
-
-
-
-
-
-
-
-
- 1
-
-
- Create Tasks
-
- Start by creating writing tasks from content ideas in the Planner module. Tasks define what content needs to be written.
-
-
-
-
-
- 2
-
-
- Generate Content
-
- Use AI to generate content from tasks. Review and edit generated content before publishing.
-
-
-
-
-
- 3
-
-
- Publish
-
- Once content is reviewed and images are generated, publish directly to WordPress or export for manual publishing.
-
-
-
-
-
-
-
- >
- );
-}
diff --git a/frontend/src/pages/Writer/Drafts.tsx b/frontend/src/pages/Writer/Drafts.tsx
deleted file mode 100644
index b84d91bf..00000000
--- a/frontend/src/pages/Writer/Drafts.tsx
+++ /dev/null
@@ -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 ;
-}
diff --git a/frontend/src/pages/account/AccountSettingsPage.tsx b/frontend/src/pages/account/AccountSettingsPage.tsx
index 19f4fdbb..1da7a5ef 100644
--- a/frontend/src/pages/account/AccountSettingsPage.tsx
+++ b/frontend/src/pages/account/AccountSettingsPage.tsx
@@ -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('');
const [success, setSuccess] = useState('');
@@ -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: },
];
- if (loading) {
- return (
- <>
-
- , color: 'blue' }}
- />
-
-
-
-
- Loading settings...
-
-
-
- >
- );
- }
-
// Page titles based on active tab
const pageTitles = {
account: { title: 'Account Information', description: 'Manage your organization and billing information' },
@@ -305,18 +286,17 @@ export default function AccountSettingsPage() {
badge={{ icon: , color: 'blue' }}
parent="Account Settings"
/>
-
- {/* Tab Content */}
- {/* Account Tab */}
- {activeTab === 'account' && (
-
- {error && (
-
- )}
+ {/* Tab Content */}
+ {/* Account Tab */}
+ {activeTab === 'account' && (
+
+ {error && (
+
+ )}
- {success && (
+ {success && (
diff --git a/frontend/src/pages/account/NotificationsPage.tsx b/frontend/src/pages/account/NotificationsPage.tsx
index 76439b29..2a57beb2 100644
--- a/frontend/src/pages/account/NotificationsPage.tsx
+++ b/frontend/src/pages/account/NotificationsPage.tsx
@@ -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 ([]);
- 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 */}
- {loading ? (
-
- ) : filteredNotifications.length === 0 ? (
+ {filteredNotifications.length === 0 ? (
diff --git a/frontend/src/pages/account/PlansAndBillingPage.tsx b/frontend/src/pages/account/PlansAndBillingPage.tsx
index 9f84b4a6..e5701cd0 100644
--- a/frontend/src/pages/account/PlansAndBillingPage.tsx
+++ b/frontend/src/pages/account/PlansAndBillingPage.tsx
@@ -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('');
const [planLoadingId, setPlanLoadingId] = useState(null);
const [purchaseLoadingId, setPurchaseLoadingId] = useState(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 (
- <>
-
- , color: 'blue' }}
- />
-
- >
- );
- }
-
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: , color: 'blue' }}
parent="Plans & Billing"
/>
-
+
{/* Activation / pending payment notice */}
{!hasActivePlan && (
@@ -893,7 +876,6 @@ export default function PlansAndBillingPage() {
)}
-
>
);
}
\ No newline at end of file
diff --git a/frontend/src/pages/account/UsageAnalyticsPage.tsx b/frontend/src/pages/account/UsageAnalyticsPage.tsx
index 653a18ed..aa37f678 100644
--- a/frontend/src/pages/account/UsageAnalyticsPage.tsx
+++ b/frontend/src/pages/account/UsageAnalyticsPage.tsx
@@ -97,7 +97,6 @@ export default function UsageAnalyticsPage() {
badge={{ icon: , color: 'blue' }}
parent="Usage & Analytics"
/>
-
{/* Quick Stats Overview */}
{creditBalance && (
diff --git a/frontend/src/pages/settings/ProfileSettingsPage.tsx b/frontend/src/pages/settings/ProfileSettingsPage.tsx
deleted file mode 100644
index a3d6121b..00000000
--- a/frontend/src/pages/settings/ProfileSettingsPage.tsx
+++ /dev/null
@@ -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 (
-
-
-
-
-
- Your Profile
-
-
- Update your personal settings - Your name, preferences, and notification choices
-
-
-
-
-
-
-
- About You
-
-
- setProfile({ ...profile, firstName: e.target.value })}
- />
-
-
- setProfile({ ...profile, lastName: e.target.value })}
- />
-
-
- setProfile({ ...profile, email: e.target.value })}
- />
-
-
- setProfile({ ...profile, phone: e.target.value })}
- />
-
-
-
-
-
- How You Like It
-
-
-
-
-
-
-
-
-
- What You Want to Hear About
-
- Choose what emails you want to receive:
-
-
-
-
- Important Updates
-
- Get notified about important changes to your account
-
-
- setProfile({ ...profile, emailNotifications: checked })}
- />
-
-
-
- Tips & Product Updates (optional)
-
- Hear about new features and content tips
-
-
- setProfile({ ...profile, marketingEmails: checked })}
- />
-
-
-
-
-
-
-
- Security
-
-
-
-
-
- );
-}
diff --git a/new-updated.md b/new-updated.md
deleted file mode 100644
index 4f6881a5..00000000
--- a/new-updated.md
+++ /dev/null
@@ -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
\ No newline at end of file
diff --git a/your-analysis.md b/your-analysis.md
deleted file mode 100644
index 534ba31f..00000000
--- a/your-analysis.md
+++ /dev/null
@@ -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
\ No newline at end of file
| | | | |