diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index dec5f257..5de2ea90 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -25,18 +25,15 @@ const Privacy = lazy(() => import("./pages/legal/Privacy")); const Home = lazy(() => import("./pages/Dashboard/Home")); // Planner Module - Lazy loaded -const PlannerDashboard = lazy(() => import("./pages/Planner/Dashboard")); const Keywords = lazy(() => import("./pages/Planner/Keywords")); const Clusters = lazy(() => import("./pages/Planner/Clusters")); const ClusterDetail = lazy(() => import("./pages/Planner/ClusterDetail")); const Ideas = lazy(() => import("./pages/Planner/Ideas")); // Writer Module - Lazy loaded -const WriterDashboard = lazy(() => import("./pages/Writer/Dashboard")); const Tasks = lazy(() => import("./pages/Writer/Tasks")); const Content = lazy(() => import("./pages/Writer/Content")); const ContentView = lazy(() => import("./pages/Writer/ContentView")); -const Drafts = lazy(() => import("./pages/Writer/Drafts")); const Images = lazy(() => import("./pages/Writer/Images")); const Review = lazy(() => import("./pages/Writer/Review")); const Approved = lazy(() => import("./pages/Writer/Approved")); @@ -45,16 +42,13 @@ const Approved = lazy(() => import("./pages/Writer/Approved")); const AutomationPage = lazy(() => import("./pages/Automation/AutomationPage")); // Linker Module - Lazy loaded -const LinkerDashboard = lazy(() => import("./pages/Linker/Dashboard")); const LinkerContentList = lazy(() => import("./pages/Linker/ContentList")); // Optimizer Module - Lazy loaded -const OptimizerDashboard = lazy(() => import("./pages/Optimizer/Dashboard")); const OptimizerContentSelector = lazy(() => import("./pages/Optimizer/ContentSelector")); const AnalysisPreview = lazy(() => import("./pages/Optimizer/AnalysisPreview")); -// Thinker Module - Lazy loaded -const ThinkerDashboard = lazy(() => import("./pages/Thinker/Dashboard")); +// Thinker Module - Lazy loaded (Admin Only) const Prompts = lazy(() => import("./pages/Thinker/Prompts")); const AuthorProfiles = lazy(() => import("./pages/Thinker/AuthorProfiles")); const ThinkerProfile = lazy(() => import("./pages/Thinker/Profile")); @@ -104,7 +98,6 @@ const PostEditor = lazy(() => import("./pages/Sites/PostEditor")); const SiteSettings = lazy(() => import("./pages/Sites/Settings")); const SyncDashboard = lazy(() => import("./pages/Sites/SyncDashboard")); const DeploymentPanel = lazy(() => import("./pages/Sites/DeploymentPanel")); -const PublishingQueue = lazy(() => import("./pages/Sites/PublishingQueue")); // Publisher Module - Lazy loaded const ContentCalendar = lazy(() => import("./pages/Publisher/ContentCalendar")); diff --git a/frontend/src/components/auth/SignUpFormEnhanced.tsx b/frontend/src/components/auth/SignUpFormEnhanced.tsx index 81718595..61bdc9b6 100644 --- a/frontend/src/components/auth/SignUpFormEnhanced.tsx +++ b/frontend/src/components/auth/SignUpFormEnhanced.tsx @@ -376,9 +376,8 @@ export default function SignUpFormEnhanced({ planDetails: planDetailsProp, planL {isPaidPlan ? ( - ) : ( - diff --git a/frontend/src/components/common/PageLoader.tsx b/frontend/src/components/common/PageLoader.tsx new file mode 100644 index 00000000..8b63b10c --- /dev/null +++ b/frontend/src/components/common/PageLoader.tsx @@ -0,0 +1,31 @@ +/** + * Page Loader Component - Global page loading indicator + * Displays a consistent spinner overlay when pages are loading + * Used in AppLayout to provide unified loading experience across all pages + */ +import React from 'react'; +import { Spinner } from '../ui/spinner/Spinner'; +import { usePageLoadingContext } from '../../context/PageLoadingContext'; + +interface PageLoaderProps { + className?: string; +} + +export const PageLoader: React.FC = ({ className = '' }) => { + const { isLoading, loadingMessage } = usePageLoadingContext(); + + if (!isLoading) return null; + + return ( +
+ + {loadingMessage && ( +

+ {loadingMessage} +

+ )} +
+ ); +}; + +export default PageLoader; diff --git a/frontend/src/components/common/SiteCard.tsx b/frontend/src/components/common/SiteCard.tsx index 26392669..09a378d6 100644 --- a/frontend/src/components/common/SiteCard.tsx +++ b/frontend/src/components/common/SiteCard.tsx @@ -52,8 +52,8 @@ export default function SiteCard({ return (
-
-
+
+
{icon}
@@ -61,32 +61,34 @@ export default function SiteCard({ {site.name}
-

- {site.description || 'No description'} -

{site.domain && ( -

+

{site.domain}

)} -
- + {site.description && ( +

+ {site.description} +

+ )} +
+ {site.industry_name && ( - + {site.industry_name} )} - + {site.active_sectors_count} / 5 Sectors {site.status && ( - + {site.status} )}
{/* Setup Checklist - Compact View */} -
+
{/* Status Text and Circle - Same row */} -
+
{statusText.text}
-
-
+
+
); }; +// Wrapper component to conditionally render Outlet or PageLoader +const PageLoaderWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const { isLoading } = usePageLoadingContext(); + + if (isLoading) { + return ; + } + + return <>{children}; +}; + const AppLayout: React.FC = () => { return ( - + + + ); }; diff --git a/frontend/src/pages/Analytics.tsx b/frontend/src/pages/Analytics.tsx deleted file mode 100644 index 04104517..00000000 --- a/frontend/src/pages/Analytics.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import PageMeta from "../components/common/PageMeta"; -import ComponentCard from "../components/common/ComponentCard"; - -export default function Analytics() { - return ( - <> - - - -
-

- Analytics - Coming Soon -

-

- Performance analytics and reporting for data-driven decisions -

-
-
- - ); -} - diff --git a/frontend/src/pages/Automation/AutomationPage_old.tsx b/frontend/src/pages/Automation/AutomationPage_old.tsx deleted file mode 100644 index e1907bfd..00000000 --- a/frontend/src/pages/Automation/AutomationPage_old.tsx +++ /dev/null @@ -1,389 +0,0 @@ -/** - * Automation Dashboard Page - * Main page for managing AI automation pipeline - */ -import React, { useState, useEffect } from 'react'; -import { useToast } from '../../components/ui/toast/ToastContainer'; -import { useSiteStore } from '../../store/siteStore'; -import { automationService, AutomationRun, AutomationConfig, PipelineStage } from '../../services/automationService'; -import StageCard from '../../components/Automation/StageCard'; -import ActivityLog from '../../components/Automation/ActivityLog'; -import ConfigModal from '../../components/Automation/ConfigModal'; -import RunHistory from '../../components/Automation/RunHistory'; -import PageMeta from '../../components/common/PageMeta'; -import ComponentCard from '../../components/common/ComponentCard'; -import DebugSiteSelector from '../../components/common/DebugSiteSelector'; -import Button from '../../components/ui/button/Button'; -import { BoltIcon } from '../../icons'; - -const STAGE_NAMES = [ - 'Keywords → Clusters', - 'Clusters → Ideas', - 'Ideas → Tasks', - 'Tasks → Content', - 'Content → Image Prompts', - 'Image Prompts → Images', - 'Manual Review Gate', -]; - -const AutomationPage: React.FC = () => { - const { activeSite } = useSiteStore(); - const toast = useToast(); - const [config, setConfig] = useState(null); - const [currentRun, setCurrentRun] = useState(null); - const [pipelineOverview, setPipelineOverview] = useState([]); - const [showConfigModal, setShowConfigModal] = useState(false); - const [loading, setLoading] = useState(true); - const [lastUpdated, setLastUpdated] = useState(new Date()); - const [estimate, setEstimate] = useState<{ estimated_credits: number; current_balance: number; sufficient: boolean } | null>(null); - - // Poll for current run updates - useEffect(() => { - if (!activeSite) return; - - loadData(); - - // Poll every 5 seconds when run is active - const interval = setInterval(() => { - if (currentRun && (currentRun.status === 'running' || currentRun.status === 'paused')) { - loadCurrentRun(); - } else { - // Refresh pipeline overview when not running - loadPipelineOverview(); - } - }, 5000); - - return () => clearInterval(interval); - }, [activeSite, currentRun?.status]); - - const loadData = async () => { - if (!activeSite) return; - - try { - setLoading(true); - const [configData, runData, estimateData, pipelineData] = await Promise.all([ - automationService.getConfig(activeSite.id), - automationService.getCurrentRun(activeSite.id), - automationService.estimate(activeSite.id), - automationService.getPipelineOverview(activeSite.id), - ]); - setConfig(configData); - setCurrentRun(runData.run); - setEstimate(estimateData); - setPipelineOverview(pipelineData.stages); - setLastUpdated(new Date()); - } catch (error: any) { - toast.error('Failed to load automation data'); - console.error(error); - } finally { - setLoading(false); - } - }; - - const loadCurrentRun = async () => { - if (!activeSite) return; - - try { - const data = await automationService.getCurrentRun(activeSite.id); - setCurrentRun(data.run); - } catch (error) { - console.error('Failed to poll current run', error); - } - }; - - const loadPipelineOverview = async () => { - if (!activeSite) return; - - try { - const data = await automationService.getPipelineOverview(activeSite.id); - setPipelineOverview(data.stages); - } catch (error) { - console.error('Failed to poll pipeline overview', error); - } - }; - - const handleRunNow = async () => { - if (!activeSite) return; - - // Check credit balance - if (estimate && !estimate.sufficient) { - toast.error(`Insufficient credits. Need ~${estimate.estimated_credits}, you have ${estimate.current_balance}`); - return; - } - - try { - const result = await automationService.runNow(activeSite.id); - toast.success('Automation started'); - loadCurrentRun(); - } catch (error: any) { - toast.error(error.response?.data?.error || 'Failed to start automation'); - } - }; - - const handlePause = async () => { - if (!currentRun) return; - - try { - await automationService.pause(currentRun.run_id); - toast.success('Automation paused'); - loadCurrentRun(); - } catch (error) { - toast.error('Failed to pause automation'); - } - }; - - const handleResume = async () => { - if (!currentRun) return; - - try { - await automationService.resume(currentRun.run_id); - toast.success('Automation resumed'); - loadCurrentRun(); - } catch (error) { - toast.error('Failed to resume automation'); - } - }; - - const handleSaveConfig = async (newConfig: Partial) => { - if (!activeSite) return; - - try { - await automationService.updateConfig(activeSite.id, newConfig); - toast.success('Configuration saved'); - setShowConfigModal(false); - loadData(); - } catch (error) { - toast.error('Failed to save configuration'); - } - }; - - if (loading) { - return ( -
-
Loading automation...
-
- ); - } - - if (!activeSite) { - return ( -
-
Please select a site to view automation
-
- ); - } - - return ( - <> - - -
- {/* Page Header with Site Selector (no sector) */} -
-
-
-
- -
-

AI Automation Pipeline

-
- {activeSite && ( -
- {lastUpdated && ( -

- Last updated: {lastUpdated.toLocaleTimeString()} -

- )} - -

- Site: {activeSite.name} -

-
- )} -
- -
- -
-
- - {/* Schedule Status Card */} - {config && ( - -
-
-
Status
-
- {config.is_enabled ? ( - ● Enabled - ) : ( - ○ Disabled - )} -
-
-
-
Schedule
-
- {config.frequency} at {config.scheduled_time} -
-
-
-
Last Run
-
- {config.last_run_at - ? new Date(config.last_run_at).toLocaleString() - : 'Never'} -
-
-
-
Estimated Credits
-
- {estimate?.estimated_credits || 0} credits - {estimate && !estimate.sufficient && ( - (Insufficient) - )} -
-
-
- -
- - {currentRun?.status === 'running' && ( - - )} - {currentRun?.status === 'paused' && ( - - )} - {!currentRun && ( - - )} -
-
- )} - - {/* Pipeline Overview - Always Visible */} - -
-
- {currentRun ? ( - <> - ● Live Run Active - Stage {currentRun.current_stage} of 7 - - ) : ( - <> - Pipeline Status - Ready to run - - )} -
-
- {pipelineOverview.reduce((sum, stage) => sum + stage.pending, 0)} total items pending -
-
- - {/* Stage Cards Grid */} -
- {STAGE_NAMES.map((name, index) => ( - - ))} -
-
- - {/* Current Run Status */} - {currentRun && ( - -
-
-
Status
-
- {currentRun.status === 'running' && ● {currentRun.status}} - {currentRun.status === 'paused' && ⏸ {currentRun.status}} - {currentRun.status === 'completed' && ✓ {currentRun.status}} - {currentRun.status === 'failed' && ✗ {currentRun.status}} -
-
-
-
Current Stage
-
- Stage {currentRun.current_stage}: {STAGE_NAMES[currentRun.current_stage - 1]} -
-
-
-
Started
-
- {new Date(currentRun.started_at).toLocaleString()} -
-
-
-
Credits Used
-
{currentRun.total_credits_used}
-
-
-
- )} - - {/* Activity Log */} - {currentRun && ( - - )} - - {/* Run History */} - - - {/* Config Modal */} - {showConfigModal && config && ( - setShowConfigModal(false)} - /> - )} -
- - ); -}; - -export default AutomationPage; diff --git a/frontend/src/pages/Billing/Credits.tsx b/frontend/src/pages/Billing/Credits.tsx index d6e5c02b..12b5b0be 100644 --- a/frontend/src/pages/Billing/Credits.tsx +++ b/frontend/src/pages/Billing/Credits.tsx @@ -7,11 +7,12 @@ import { Card } from '../../components/ui/card'; import Badge from '../../components/ui/badge/Badge'; import Button from '../../components/ui/button/Button'; import { ArrowUpIcon } from '../../icons'; +import { usePageLoading } from '../../context/PageLoadingContext'; export default function Credits() { const toast = useToast(); + const { startLoading, stopLoading } = usePageLoading(); const [balance, setBalance] = useState(null); - const [loading, setLoading] = useState(true); useEffect(() => { loadBalance(); @@ -19,29 +20,18 @@ export default function Credits() { const loadBalance = async () => { try { - setLoading(true); + startLoading('Loading content usage...'); const data = await getCreditBalance(); setBalance(data); } catch (error: any) { toast.error(`Failed to load content usage: ${error.message}`); } finally { - setLoading(false); + stopLoading(); } }; - if (loading) { - return ( -
- -
-
Loading...
-
-
- ); - } - return ( -
+ <>
@@ -126,7 +116,7 @@ export default function Credits() {
-
+ ); } diff --git a/frontend/src/pages/Billing/Transactions.tsx b/frontend/src/pages/Billing/Transactions.tsx index 82b012c8..6afcad23 100644 --- a/frontend/src/pages/Billing/Transactions.tsx +++ b/frontend/src/pages/Billing/Transactions.tsx @@ -4,11 +4,12 @@ import { useToast } from '../../components/ui/toast/ToastContainer'; import { getCreditTransactions, CreditTransaction } from '../../services/billing.api'; import { Card } from '../../components/ui/card'; import Badge from '../../components/ui/badge/Badge'; +import { usePageLoading } from '../../context/PageLoadingContext'; export default function Transactions() { const toast = useToast(); + const { startLoading, stopLoading } = usePageLoading(); const [transactions, setTransactions] = useState([]); - const [loading, setLoading] = useState(true); const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(1); @@ -18,7 +19,7 @@ export default function Transactions() { const loadTransactions = async () => { try { - setLoading(true); + startLoading('Loading transactions...'); const response = await getCreditTransactions(); setTransactions(response.results || []); const count = response.count || 0; @@ -26,7 +27,7 @@ export default function Transactions() { } catch (error: any) { toast.error(`Failed to load transactions: ${error.message}`); } finally { - setLoading(false); + stopLoading(); } }; @@ -47,47 +48,42 @@ export default function Transactions() { }; return ( -
+ <>

Credit Transactions

View all credit transactions and history

- {loading ? ( -
-
Loading...
-
- ) : ( - -
- - - - - - - - - - - - {transactions.map((transaction) => ( - - - -
DateTypeAmountReferenceDescription
- {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()} + +
+ + + + + + + + + + + + {transactions.map((transaction) => ( + + + +
DateTypeAmountReferenceDescription
+ {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 ( -
- -
-
Loading...
-
-
- ); - } - 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 ? ( -
-
-

Loading content...

-
- ) : ( -
-
- - - - - -
- 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 ? ( -
-
-

Loading stats...

-
- ) : 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

-
-
- - -
-
- - ) : ( -
-

No data available

-
- )} -
- - ); -} - 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 ? ( -
-
-

Loading stats...

-
- ) : 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

-
-
- - -
-
- - ) : ( -
-

No data available

-
- )} -
- - ); -} - 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 ( -
+ <>
Loading cluster...
-
+ ); } 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 ( -
+ <>
Loading calendar...
-
+ ); } 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 ? ( -
-
Loading...
-
- ) : ( - -
- - - - - - - - - - - - - {keywords.map((keyword) => ( - - - +
KeywordIndustrySectorVolumeDifficultyCountry
- {keyword.keyword} - - {keyword.industry_name} + +
+ + + + + + + + + + + + + {keywords.map((keyword) => ( + + + +
KeywordIndustrySectorVolumeDifficultyCountry
+ {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 ? ( -
-
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 ( -
+ <>

Loading billing data...

-
+ ); } 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 ? ( -
-
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 ? ( -
-
Loading plans...
-
- ) : 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 ( -
- -
-
Loading...
-
-
- ); - } - 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 */} - -
- - - - - - - - - - - {Object.entries(status.processes?.by_stack || {}).map(([stack, stats]) => ( - - - - - - - ))} - -
StackProcessesCPU %Memory (MB)
{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 ? ( -
-
Loading...
-
- ) : ( - -
- - - - - - - + +
+
AccountStatusPeriod StartPeriod End
+ + + + + + + + + + {subscriptions.map((subscription) => ( + + + + + - - - {subscriptions.map((subscription) => ( - - - - - - - ))} - -
AccountStatusPeriod StartPeriod End
{subscription.account_name} + + {subscription.status} + + + {new Date(subscription.current_period_start).toLocaleDateString()} + + {new Date(subscription.current_period_end).toLocaleDateString()} +
{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 ? ( -
-
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 ? ( -
-
Loading...
-
- ) : ( - -
- - - - - - - + +
+
EmailUsernameRoleStatus
+ + + + + + + + + + {users.map((user) => ( + + + + + - - - {users.map((user) => ( - - - - - - - ))} - -
EmailUsernameRoleStatus
{user.email}{user.username} + {user.role} + + + {user.is_active ? 'Active' : 'Inactive'} + +
{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' }} - /> -
-
-
Loading...
-
-
- - ); - } - // 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 ( -
+ <>
Loading sites...
-
+ ); } 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 ( -
+ <>
Loading pages...
-
+ ); } 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 ( -
+ <>
Loading post...
-
+ ); } 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 ( -
- -
-
Loading queue...
-
-
- ); - } - - 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 ( -
+ <>
Loading site settings...
-
+ ); } 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 ( -
+ <>
Loading sync data...
-
+ ); } 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 ? ( -
-
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.count} - -
- )} - {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 && ( -
-

{error}

-
- )} + {/* Tab Content */} + {/* Account Tab */} + {activeTab === 'account' && ( +
+ {error && ( +
+

{error}

+
+ )} - {success && ( + {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

-
-
- setProfile({ ...profile, language: val })} - /> -
-
-
- - -

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