NAVIGATION_REFACTOR COMPLETED

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-17 03:49:50 +00:00
parent 47a00e8875
commit 501a269450
29 changed files with 3839 additions and 2103 deletions

View File

@@ -48,6 +48,8 @@ const Approved = lazy(() => import("./pages/Writer/Approved"));
// Automation Module - Lazy loaded
const AutomationPage = lazy(() => import("./pages/Automation/AutomationPage"));
const AutomationOverview = lazy(() => import("./pages/Automation/AutomationOverview"));
const PipelineSettings = lazy(() => import("./pages/Automation/PipelineSettings"));
// Linker Module - Lazy loaded
const LinkerContentList = lazy(() => import("./pages/Linker/ContentList"));
@@ -110,6 +112,7 @@ const DeploymentPanel = lazy(() => import("./pages/Sites/DeploymentPanel"));
// Publisher Module - Lazy loaded
const ContentCalendar = lazy(() => import("./pages/Publisher/ContentCalendar"));
const PublishSettings = lazy(() => import("./pages/Publisher/PublishSettings"));
// Setup - Lazy loaded
const SetupWizard = lazy(() => import("./pages/Setup/SetupWizard"));
@@ -193,11 +196,15 @@ export default function App() {
<Route path="/writer/published" element={<Navigate to="/writer/approved" replace />} />
{/* Automation Module */}
<Route path="/automation" element={<AutomationPage />} />
<Route path="/automation" element={<Navigate to="/automation/overview" replace />} />
<Route path="/automation/overview" element={<AutomationOverview />} />
<Route path="/automation/settings" element={<PipelineSettings />} />
<Route path="/automation/run" element={<AutomationPage />} />
{/* Publisher Module - Content Calendar */}
{/* Publisher Module - Content Calendar & Settings */}
<Route path="/publisher" element={<Navigate to="/publisher/content-calendar" replace />} />
<Route path="/publisher/content-calendar" element={<ContentCalendar />} />
<Route path="/publisher/settings" element={<PublishSettings />} />
{/* Linker Module - Redirect dashboard to content */}
<Route path="/linker" element={<Navigate to="/linker/content" replace />} />

View File

@@ -111,7 +111,7 @@ export const createClustersPageConfig = (
render: (value: string, row: Cluster) => (
<Link
to={`/planner/clusters/${row.id}`}
className="text-base font-light text-brand-600 hover:text-brand-700 dark:text-brand-400 dark:hover:text-brand-300"
className="text-sm font-medium text-brand-600 hover:text-brand-700 dark:text-brand-400 dark:hover:text-brand-300"
>
{value}
</Link>

View File

@@ -109,7 +109,7 @@ export const createIdeasPageConfig = (
toggleContentKey: 'description', // Use description field for toggle content
toggleContentLabel: 'Content Outline', // Label for expanded content
render: (value: string) => (
<span className="text-gray-800 dark:text-white text-base font-light">{value}</span>
<span className="text-sm font-medium text-gray-900 dark:text-white">{value}</span>
),
},
// Sector column - only show when viewing all sectors

View File

@@ -153,7 +153,7 @@ export const createKeywordsPageConfig = (
sortField: 'seed_keyword__keyword',
width: '300px',
render: (value: string) => (
<span>
<span className="text-sm font-medium text-gray-900 dark:text-white">
{value || '-'}
</span>
),

View File

@@ -113,7 +113,7 @@ export const createTasksPageConfig = (
return (
<div className="flex items-center gap-2">
<span className="text-base font-light text-gray-900 dark:text-white">
<span className="text-sm font-medium text-gray-900 dark:text-white">
{displayTitle}
</span>
</div>

View File

@@ -7,6 +7,7 @@ export const titleColumn = {
key: 'title',
label: 'Title',
sortable: true,
className: 'text-sm',
};
export const keywordColumn = {

View File

@@ -148,28 +148,35 @@ const AppSidebar: React.FC = () => {
{ name: "Content Queue", path: "/writer/tasks" },
{ name: "Content Drafts", path: "/writer/content" },
{ name: "Content Images", path: "/writer/images" },
{ name: "Content Review", path: "/writer/review" },
{ name: "Content Approved", path: "/writer/approved" },
],
});
}
// Add Automation if enabled (no dropdown - single page)
// Add Publisher (after Writer) - always visible
workflowItems.push({
icon: <CalendarIcon />,
name: "Publisher",
subItems: [
{ name: "Content Review", path: "/writer/review" },
{ name: "Publish / Schedule", path: "/writer/approved" },
{ name: "Publish Settings", path: "/publisher/settings" },
{ name: "Content Calendar", path: "/publisher/content-calendar" },
],
});
// Add Automation if enabled (with dropdown)
if (isModuleEnabled('automation')) {
workflowItems.push({
icon: <BoltIcon />,
name: "Automation",
path: "/automation",
subItems: [
{ name: "Overview", path: "/automation/overview" },
{ name: "Settings", path: "/automation/settings" },
{ name: "Run Now (Manual)", path: "/automation/run" },
],
});
}
// Add Content Calendar (Publisher) - always visible
workflowItems.push({
icon: <CalendarIcon />,
name: "Content Calendar",
path: "/publisher/content-calendar",
});
// Linker and Optimizer removed - not active modules
return [
@@ -195,19 +202,10 @@ const AppSidebar: React.FC = () => {
{
label: "ACCOUNT",
items: [
{
icon: <Bell className="w-5 h-5" />,
name: "Notifications",
path: "/account/notifications",
},
{
icon: <UserCircleIcon />,
name: "Account Settings",
subItems: [
{ name: "Account", path: "/account/settings" },
{ name: "Profile", path: "/account/settings/profile" },
{ name: "Team", path: "/account/settings/team" },
],
path: "/account/settings", // Single page, no sub-items
},
{
icon: <DollarLineIcon />,
@@ -230,6 +228,11 @@ const AppSidebar: React.FC = () => {
{
label: "HELP",
items: [
{
icon: <Bell className="w-5 h-5" />,
name: "Notifications",
path: "/account/notifications",
},
{
icon: <DocsIcon />,
name: "Help & Docs",
@@ -503,7 +506,7 @@ const AppSidebar: React.FC = () => {
)}
</Link>
</div>
<div className="flex flex-col overflow-y-auto duration-300 ease-linear no-scrollbar">
<div className="flex flex-col overflow-y-auto duration-300 ease-linear no-scrollbar mt-[50px]">
<nav>
<div className="flex flex-col gap-1">
{allSections.map((section, sectionIndex) => (

View File

@@ -0,0 +1,289 @@
/**
* Automation Overview Page
* Comprehensive dashboard showing automation status, metrics, cost estimation, and run history
*/
import React, { useState, useEffect } from 'react';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { useSiteStore } from '../../store/siteStore';
import { automationService } from '../../services/automationService';
import {
fetchKeywords,
fetchClusters,
fetchContentIdeas,
fetchTasks,
fetchContent,
fetchImages,
} from '../../services/api';
import RunHistory from '../../components/Automation/RunHistory';
import PageMeta from '../../components/common/PageMeta';
import PageHeader from '../../components/common/PageHeader';
import ComponentCard from '../../components/common/ComponentCard';
import {
ListIcon,
GroupIcon,
FileTextIcon,
FileIcon,
BoltIcon,
} from '../../icons';
const AutomationOverview: React.FC = () => {
const { activeSite } = useSiteStore();
const toast = useToast();
const [loading, setLoading] = useState(true);
const [metrics, setMetrics] = useState<any>(null);
const [estimate, setEstimate] = useState<any>(null);
// Load metrics for the 5 metric cards
const loadMetrics = async () => {
if (!activeSite) return;
try {
const [
keywordsTotalRes, keywordsNewRes, keywordsMappedRes,
clustersTotalRes, clustersNewRes, clustersMappedRes,
ideasTotalRes, ideasNewRes, ideasQueuedRes, ideasCompletedRes,
tasksTotalRes,
contentTotalRes, contentDraftRes, contentReviewRes, contentPublishedRes,
contentNotPublishedRes, contentScheduledRes,
imagesTotalRes, imagesPendingRes,
] = await Promise.all([
fetchKeywords({ page_size: 1, site_id: activeSite.id }),
fetchKeywords({ page_size: 1, site_id: activeSite.id, status: 'new' }),
fetchKeywords({ page_size: 1, site_id: activeSite.id, status: 'mapped' }),
fetchClusters({ page_size: 1, site_id: activeSite.id }),
fetchClusters({ page_size: 1, site_id: activeSite.id, status: 'new' }),
fetchClusters({ page_size: 1, site_id: activeSite.id, status: 'mapped' }),
fetchContentIdeas({ page_size: 1, site_id: activeSite.id }),
fetchContentIdeas({ page_size: 1, site_id: activeSite.id, status: 'new' }),
fetchContentIdeas({ page_size: 1, site_id: activeSite.id, status: 'queued' }),
fetchContentIdeas({ page_size: 1, site_id: activeSite.id, status: 'completed' }),
fetchTasks({ page_size: 1, site_id: activeSite.id }),
fetchContent({ page_size: 1, site_id: activeSite.id }),
fetchContent({ page_size: 1, site_id: activeSite.id, status: 'draft' }),
fetchContent({ page_size: 1, site_id: activeSite.id, status: 'review' }),
fetchContent({ page_size: 1, site_id: activeSite.id, status__in: 'approved,published' }),
fetchContent({ page_size: 1, site_id: activeSite.id, status: 'approved' }),
fetchContent({ page_size: 1, site_id: activeSite.id, status: 'approved' }),
fetchImages({ page_size: 1 }),
fetchImages({ page_size: 1, status: 'pending' }),
]);
setMetrics({
keywords: { total: keywordsTotalRes.count || 0, new: keywordsNewRes.count || 0, mapped: keywordsMappedRes.count || 0 },
clusters: { total: clustersTotalRes.count || 0, new: clustersNewRes.count || 0, mapped: clustersMappedRes.count || 0 },
ideas: { total: ideasTotalRes.count || 0, new: ideasNewRes.count || 0, queued: ideasQueuedRes.count || 0, completed: ideasCompletedRes.count || 0 },
tasks: { total: tasksTotalRes.count || 0 },
content: {
total: contentTotalRes.count || 0,
draft: contentDraftRes.count || 0,
review: contentReviewRes.count || 0,
published: contentPublishedRes.count || 0,
not_published: contentNotPublishedRes.count || 0,
scheduled: contentScheduledRes.count || 0,
},
images: { total: imagesTotalRes.count || 0, pending: imagesPendingRes.count || 0 },
});
} catch (e) {
console.warn('Failed to fetch metrics for automation overview', e);
}
};
// Load cost estimate
const loadEstimate = async () => {
if (!activeSite) return;
try {
const estimateData = await automationService.estimate(activeSite.id);
setEstimate(estimateData);
} catch (e) {
console.warn('Failed to fetch cost estimate', e);
}
};
useEffect(() => {
const loadData = async () => {
setLoading(true);
await Promise.all([loadMetrics(), loadEstimate()]);
setLoading(false);
};
if (activeSite) {
loadData();
}
}, [activeSite]);
// Helper to render metric rows
const renderMetricRow = (items: Array<{ label: string; value: number; colorCls: string }>) => {
return (
<div className="flex justify-between text-xs mt-2">
{items.map((item, idx) => (
<div key={idx} className="flex items-baseline gap-1">
<span className="text-gray-500 dark:text-gray-400">{item.label}</span>
<span className={`font-semibold ${item.colorCls}`}>{item.value}</span>
</div>
))}
</div>
);
};
if (!activeSite) {
return (
<div className="p-6">
<p className="text-gray-600 dark:text-gray-400">Please select a site to view automation overview.</p>
</div>
);
}
if (loading) {
return (
<div className="flex items-center justify-center min-h-[60vh]">
<div className="text-gray-500 dark:text-gray-400">Loading automation overview...</div>
</div>
);
}
return (
<>
<PageMeta title="Automation Overview" description="Comprehensive automation dashboard" />
<div className="space-y-6">
<PageHeader
title="Automation Overview"
breadcrumb="Automation / Overview"
description="Comprehensive automation dashboard with metrics, cost estimation, and run history"
/>
{/* Metrics Summary Cards */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
{/* Keywords */}
<div className="bg-white dark:bg-gray-900 rounded-xl p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div className="size-8 rounded-lg bg-brand-100 dark:bg-brand-900/30 flex items-center justify-center">
<ListIcon className="size-4 text-brand-600 dark:text-brand-400" />
</div>
<div className="text-base font-bold text-gray-900 dark:text-white">Keywords</div>
</div>
<div className="text-right">
<div className="text-3xl font-bold text-brand-600">{metrics?.keywords?.total || 0}</div>
</div>
</div>
{renderMetricRow([
{ label: 'New:', value: metrics?.keywords?.new || 0, colorCls: 'text-brand-600' },
{ label: 'Mapped:', value: metrics?.keywords?.mapped || 0, colorCls: 'text-brand-600' },
])}
</div>
{/* Clusters */}
<div className="bg-white dark:bg-gray-900 rounded-xl p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div className="size-8 rounded-lg bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center">
<GroupIcon className="size-4 text-purple-600 dark:text-purple-400" />
</div>
<div className="text-base font-bold text-gray-900 dark:text-white">Clusters</div>
</div>
<div className="text-right">
<div className="text-3xl font-bold text-purple-600">{metrics?.clusters?.total || 0}</div>
</div>
</div>
{renderMetricRow([
{ label: 'New:', value: metrics?.clusters?.new || 0, colorCls: 'text-purple-600' },
{ label: 'Mapped:', value: metrics?.clusters?.mapped || 0, colorCls: 'text-purple-600' },
])}
</div>
{/* Ideas */}
<div className="bg-white dark:bg-gray-900 rounded-xl p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div className="size-8 rounded-lg bg-warning-100 dark:bg-warning-900/30 flex items-center justify-center">
<BoltIcon className="size-4 text-warning-600 dark:text-warning-400" />
</div>
<div className="text-base font-bold text-gray-900 dark:text-white">Ideas</div>
</div>
<div className="text-right">
<div className="text-3xl font-bold text-warning-600">{metrics?.ideas?.total || 0}</div>
</div>
</div>
{renderMetricRow([
{ label: 'New:', value: metrics?.ideas?.new || 0, colorCls: 'text-warning-600' },
{ label: 'Queued:', value: metrics?.ideas?.queued || 0, colorCls: 'text-warning-600' },
{ label: 'Done:', value: metrics?.ideas?.completed || 0, colorCls: 'text-warning-600' },
])}
</div>
{/* Content */}
<div className="bg-white dark:bg-gray-900 rounded-xl p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div className="size-8 rounded-lg bg-success-100 dark:bg-success-900/30 flex items-center justify-center">
<FileTextIcon className="size-4 text-success-600 dark:text-success-400" />
</div>
<div className="text-base font-bold text-gray-900 dark:text-white">Content</div>
</div>
<div className="text-right">
<div className="text-3xl font-bold text-success-600">{metrics?.content?.total || 0}</div>
</div>
</div>
{renderMetricRow([
{ label: 'Draft:', value: metrics?.content?.draft || 0, colorCls: 'text-success-600' },
{ label: 'Review:', value: metrics?.content?.review || 0, colorCls: 'text-success-600' },
{ label: 'Publish:', value: metrics?.content?.published || 0, colorCls: 'text-success-600' },
])}
</div>
{/* Images */}
<div className="bg-white dark:bg-gray-900 rounded-xl p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div className="size-8 rounded-lg bg-info-100 dark:bg-info-900/30 flex items-center justify-center">
<FileIcon className="size-4 text-info-600 dark:text-info-400" />
</div>
<div className="text-base font-bold text-gray-900 dark:text-white">Images</div>
</div>
<div className="text-right">
<div className="text-3xl font-bold text-info-600">{metrics?.images?.total || 0}</div>
</div>
</div>
{renderMetricRow([
{ label: 'Pending:', value: metrics?.images?.pending || 0, colorCls: 'text-info-600' },
])}
</div>
</div>
{/* Cost Estimation Card */}
{estimate && (
<ComponentCard
title="Ready to Process"
>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<div className="space-y-3">
<div className="text-sm font-medium text-gray-700 dark:text-gray-300">
Estimated Items to Process: <span className="text-lg font-bold text-brand-600">{estimate.estimated_credits || 0}</span>
</div>
<div className="text-sm font-medium text-gray-700 dark:text-gray-300">
Current Balance: <span className="text-lg font-bold text-success-600">{estimate.current_balance || 0}</span> credits
</div>
<div className="text-sm font-medium text-gray-700 dark:text-gray-300">
Status: {estimate.sufficient ? (
<span className="text-success-600 font-bold"> Sufficient credits</span>
) : (
<span className="text-danger-600 font-bold"> Insufficient credits</span>
)}
</div>
</div>
</div>
</div>
</ComponentCard>
)}
{/* Run History */}
{activeSite && <RunHistory siteId={activeSite.id} />}
</div>
</>
);
};
export default AutomationOverview;

View File

@@ -15,14 +15,11 @@ import {
fetchImages,
} from '../../services/api';
import ActivityLog from '../../components/Automation/ActivityLog';
import ConfigModal from '../../components/Automation/ConfigModal';
import RunHistory from '../../components/Automation/RunHistory';
import CurrentProcessingCard from '../../components/Automation/CurrentProcessingCardV2';
import GlobalProgressBar, { getProcessedFromResult } from '../../components/Automation/GlobalProgressBar';
import PageMeta from '../../components/common/PageMeta';
import PageHeader from '../../components/common/PageHeader';
import ComponentCard from '../../components/common/ComponentCard';
import EnhancedMetricCard from '../../components/dashboard/EnhancedMetricCard';
import DebugSiteSelector from '../../components/common/DebugSiteSelector';
import Button from '../../components/ui/button/Button';
import {
@@ -67,7 +64,6 @@ const AutomationPage: React.FC = () => {
const [currentRun, setCurrentRun] = useState<AutomationRun | null>(null);
const [pipelineOverview, setPipelineOverview] = useState<PipelineStage[]>([]);
const [metrics, setMetrics] = useState<any>(null);
const [showConfigModal, setShowConfigModal] = useState(false);
const [showProcessingCard, setShowProcessingCard] = useState<boolean>(true);
const [loading, setLoading] = useState(true);
const [estimate, setEstimate] = useState<{ estimated_credits: number; current_balance: number; sufficient: boolean } | null>(null);
@@ -443,23 +439,6 @@ const AutomationPage: React.FC = () => {
}
};
const handleSaveConfig = async (newConfig: Partial<AutomationConfig>) => {
if (!activeSite) return;
try {
await automationService.updateConfig(activeSite.id, newConfig);
toast.success('Configuration saved');
setShowConfigModal(false);
// Optimistically update config locally and refresh data
setConfig((prev) => ({ ...(prev as AutomationConfig), ...newConfig } as AutomationConfig));
await loadPipelineOverview();
await loadMetrics();
await loadCurrentRun();
} catch (error) {
console.error('Failed to save config:', error);
toast.error('Failed to save configuration');
}
};
const handlePublishAllWithoutReview = async () => {
if (!activeSite) return;
if (!confirm('Publish all content without review? This cannot be undone.')) return;
@@ -640,13 +619,13 @@ const AutomationPage: React.FC = () => {
<div className="flex items-center gap-2">
<Button
onClick={() => setShowConfigModal(true)}
onClick={() => window.location.href = '/automation/settings'}
variant="outline"
tone="brand"
size="sm"
className="!border-white !text-white hover:!bg-white hover:!text-brand-700"
>
Configure
Pipeline Settings
</Button>
{currentRun?.status === 'running' && (
<Button
@@ -687,179 +666,6 @@ const AutomationPage: React.FC = () => {
</ComponentCard>
)}
{/* Metrics Summary Cards */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
{/* Keywords */}
<div className="bg-white dark:bg-gray-900 rounded-xl p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div className="size-8 rounded-lg bg-brand-100 dark:bg-brand-900/30 flex items-center justify-center">
<ListIcon className="size-4 text-brand-600 dark:text-brand-400" />
</div>
<div className="text-base font-bold text-gray-900 dark:text-white">Keywords</div>
</div>
{(() => {
const res = getStageResult(1);
const total = res?.total ?? pipelineOverview[0]?.counts?.total ?? metrics?.keywords?.total ?? pipelineOverview[0]?.pending ?? 0;
return (
<div className="text-right">
<div className="text-3xl font-bold text-brand-600">{total}</div>
</div>
);
})()}
</div>
{(() => {
const res = getStageResult(1);
const newCount = res?.new ?? res?.new_items ?? pipelineOverview[0]?.counts?.new ?? metrics?.keywords?.new ?? 0;
const mapped = res?.mapped ?? pipelineOverview[0]?.counts?.mapped ?? metrics?.keywords?.mapped ?? 0;
return (
renderMetricRow([
{ label: 'New:', value: newCount, colorCls: 'text-brand-600' },
{ label: 'Mapped:', value: mapped, colorCls: 'text-brand-600' },
])
);
})()}
</div>
{/* Clusters */}
<div className="bg-white dark:bg-gray-900 rounded-xl p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div className="size-8 rounded-lg bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center">
<GroupIcon className="size-4 text-purple-600 dark:text-purple-400" />
</div>
<div className="text-base font-bold text-gray-900 dark:text-white">Clusters</div>
</div>
{(() => {
const res = getStageResult(2);
const total = res?.total ?? pipelineOverview[1]?.counts?.total ?? metrics?.clusters?.total ?? pipelineOverview[1]?.pending ?? 0;
return (
<div className="text-right">
<div className="text-3xl font-bold text-purple-600">{total}</div>
</div>
);
})()}
</div>
{(() => {
const res = getStageResult(2);
const newCount = res?.new ?? res?.new_items ?? pipelineOverview[1]?.counts?.new ?? metrics?.clusters?.new ?? 0;
const mapped = res?.mapped ?? pipelineOverview[1]?.counts?.mapped ?? metrics?.clusters?.mapped ?? 0;
return (
renderMetricRow([
{ label: 'New:', value: newCount, colorCls: 'text-purple-600' },
{ label: 'Mapped:', value: mapped, colorCls: 'text-purple-600' },
])
);
})()}
</div>
{/* Ideas */}
<div className="bg-white dark:bg-gray-900 rounded-xl p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div className="size-8 rounded-lg bg-warning-100 dark:bg-warning-900/30 flex items-center justify-center">
<BoltIcon className="size-4 text-warning-600 dark:text-warning-400" />
</div>
<div className="text-base font-bold text-gray-900 dark:text-white">Ideas</div>
</div>
{(() => {
const res = getStageResult(3);
const total = res?.total ?? pipelineOverview[2]?.counts?.total ?? metrics?.ideas?.total ?? pipelineOverview[2]?.pending ?? 0;
return (
<div className="text-right">
<div className="text-3xl font-bold text-warning-600">{total}</div>
</div>
);
})()}
</div>
{(() => {
const res = getStageResult(3);
const newCount = res?.new ?? res?.new_items ?? pipelineOverview[2]?.counts?.new ?? metrics?.ideas?.new ?? 0;
const queued = res?.queued ?? pipelineOverview[2]?.counts?.queued ?? metrics?.ideas?.queued ?? 0;
const completed = res?.completed ?? pipelineOverview[2]?.counts?.completed ?? metrics?.ideas?.completed ?? 0;
return (
renderMetricRow([
{ label: 'New:', value: newCount, colorCls: 'text-warning-600' },
{ label: 'Queued:', value: queued, colorCls: 'text-warning-600' },
{ label: 'Completed:', value: completed, colorCls: 'text-warning-600' },
])
);
})()}
</div>
{/* Content */}
<div className="bg-white dark:bg-gray-900 rounded-xl p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div className="size-8 rounded-lg bg-success-100 dark:bg-success-900/30 flex items-center justify-center">
<FileTextIcon className="size-4 text-success-600 dark:text-success-400" />
</div>
<div className="text-base font-bold text-gray-900 dark:text-white">Content</div>
</div>
{(() => {
const res = getStageResult(4);
const total = res?.total ?? pipelineOverview[3]?.counts?.total ?? metrics?.content?.total ?? pipelineOverview[3]?.pending ?? 0;
return (
<div className="text-right">
<div className="text-3xl font-bold text-success-600">{total}</div>
</div>
);
})()}
</div>
{(() => {
const res = getStageResult(4);
const draft = res?.draft ?? res?.drafts ?? pipelineOverview[3]?.counts?.draft ?? metrics?.content?.draft ?? 0;
const review = res?.review ?? res?.in_review ?? pipelineOverview[3]?.counts?.review ?? metrics?.content?.review ?? 0;
const publish = res?.published ?? res?.publish ?? pipelineOverview[3]?.counts?.published ?? metrics?.content?.published ?? 0;
return (
renderMetricRow([
{ label: 'Draft:', value: draft, colorCls: 'text-success-600' },
{ label: 'Review:', value: review, colorCls: 'text-success-600' },
{ label: 'Publish:', value: publish, colorCls: 'text-success-600' },
])
);
})()}
</div>
{/* Images */}
<div className="bg-white dark:bg-gray-900 rounded-xl p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<div className="size-8 rounded-lg bg-info-100 dark:bg-info-900/30 flex items-center justify-center">
<FileIcon className="size-4 text-info-600 dark:text-info-400" />
</div>
<div className="text-base font-bold text-gray-900 dark:text-white">Images</div>
</div>
{(() => {
const res = getStageResult(6);
const total = res?.total ?? pipelineOverview[5]?.counts?.total ?? metrics?.images?.total ?? pipelineOverview[5]?.pending ?? 0;
return (
<div className="text-right">
<div className="text-3xl font-bold text-info-600">{total}</div>
</div>
);
})()}
</div>
{(() => {
const res = getStageResult(6); // stage 6 is Image Prompts -> Images
if (res && typeof res === 'object') {
const entries = Object.entries(res);
const items = entries.slice(0,3).map(([k, v]) => ({ label: `${k.replace(/_/g, ' ')}:`, value: Number(v) || 0, colorCls: 'text-info-600' }));
return renderMetricRow(items);
}
const counts = pipelineOverview[5]?.counts ?? metrics?.images ?? null;
if (counts && typeof counts === 'object') {
const entries = Object.entries(counts);
const items = entries.slice(0,3).map(([k, v]) => ({ label: `${k.replace(/_/g, ' ')}:`, value: Number(v) || 0, colorCls: 'text-info-600' }));
return renderMetricRow(items);
}
return renderMetricRow([
{ label: 'Pending:', value: pipelineOverview[5]?.pending ?? metrics?.images?.pending ?? 0, colorCls: 'text-info-600' },
]);
})()}
</div>
</div>
{/* Global Progress Bar - Shows full pipeline progress during automation run */}
{currentRun && (currentRun.status === 'running' || currentRun.status === 'paused') && (
<GlobalProgressBar
@@ -1271,7 +1077,7 @@ const AutomationPage: React.FC = () => {
);
})()}
{/* Scheduled summary card - Same layout as Stage cards */}
{/* Stage 8 card - Scheduled summary card - Same layout as Stage cards */}
<div className="rounded-xl border border-gray-200 dark:border-gray-800 p-4 transition-all bg-white dark:bg-gray-900 border-l-[5px] border-l-success-500">
{/* Header Row - Icon, Label on left; Function Name on right */}
<div className="flex items-center justify-between mb-4">
@@ -1279,10 +1085,10 @@ const AutomationPage: React.FC = () => {
<div className="size-8 rounded-lg bg-gradient-to-br from-success-500 to-success-600 flex items-center justify-center shadow-md flex-shrink-0">
<ClockIcon className="size-4 text-white" />
</div>
<span className="text-base font-bold text-gray-900 dark:text-white">Scheduled</span>
<span className="text-base font-bold text-gray-900 dark:text-white">Stage 8</span>
</div>
{/* Stage Function Name - Right side, larger font */}
<div className="text-sm font-bold text-success-600 dark:text-success-400">Ready to Publish</div>
<div className="text-sm font-bold text-success-600 dark:text-success-400">Approved Scheduled</div>
</div>
{/* Single Row: Pending & Scheduled */}
@@ -1308,14 +1114,6 @@ const AutomationPage: React.FC = () => {
{/* Activity Log */}
{currentRun && <ActivityLog runId={currentRun.run_id} />}
{/* Run History */}
<RunHistory siteId={activeSite.id} />
{/* Config Modal */}
{showConfigModal && config && (
<ConfigModal config={config} onSave={handleSaveConfig} onCancel={() => setShowConfigModal(false)} />
)}
</div>
)}
</>

View File

@@ -0,0 +1,399 @@
/**
* Automation Pipeline Settings Page
* Configure 7-stage automation pipeline (extracted from ConfigModal)
*/
import React, { useState, useEffect } 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 Checkbox from '../../components/form/input/Checkbox';
import Select from '../../components/form/Select';
import InputField from '../../components/form/input/InputField';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { useSiteStore } from '../../store/siteStore';
import { automationService, AutomationConfig } from '../../services/automationService';
import { Loader2Icon } from '../../icons';
export default function PipelineSettings() {
const navigate = useNavigate();
const toast = useToast();
const { activeSite } = useSiteStore();
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [config, setConfig] = useState<AutomationConfig | null>(null);
const [formData, setFormData] = useState<Partial<AutomationConfig>>({});
// Load config for active site
const loadConfig = async () => {
if (!activeSite) return;
try {
setLoading(true);
const configData = await automationService.getConfig(activeSite.id);
setConfig(configData);
setFormData({
is_enabled: configData.is_enabled,
frequency: configData.frequency,
scheduled_time: configData.scheduled_time,
stage_1_enabled: configData.stage_1_enabled ?? true,
stage_2_enabled: configData.stage_2_enabled ?? true,
stage_3_enabled: configData.stage_3_enabled ?? true,
stage_4_enabled: configData.stage_4_enabled ?? true,
stage_5_enabled: configData.stage_5_enabled ?? true,
stage_6_enabled: configData.stage_6_enabled ?? true,
stage_7_enabled: configData.stage_7_enabled ?? true,
stage_1_batch_size: configData.stage_1_batch_size,
stage_2_batch_size: configData.stage_2_batch_size,
stage_3_batch_size: configData.stage_3_batch_size,
stage_4_batch_size: configData.stage_4_batch_size,
stage_5_batch_size: configData.stage_5_batch_size,
stage_6_batch_size: configData.stage_6_batch_size,
within_stage_delay: configData.within_stage_delay || 3,
between_stage_delay: configData.between_stage_delay || 5,
});
} catch (error: any) {
console.error('Failed to load pipeline config:', error);
toast.error('Failed to load pipeline configuration');
} finally {
setLoading(false);
}
};
// Save config
const saveConfig = async () => {
if (!activeSite) return;
try {
setSaving(true);
const dataToSave = {
...formData,
within_stage_delay: formData.within_stage_delay || 3,
between_stage_delay: formData.between_stage_delay || 5,
};
await automationService.updateConfig(activeSite.id, dataToSave);
// Reload config after save
await loadConfig();
toast.success('Pipeline configuration saved successfully');
} catch (error: any) {
console.error('Failed to save pipeline config:', error);
toast.error('Failed to save pipeline configuration');
} finally {
setSaving(false);
}
};
// Load config on mount and when active site changes
useEffect(() => {
if (activeSite) {
loadConfig();
}
}, [activeSite]);
if (!activeSite) {
return (
<div className="p-6">
<p className="text-gray-600 dark:text-gray-400">Please select a site to configure pipeline settings.</p>
</div>
);
}
if (loading) {
return (
<div className="flex items-center justify-center min-h-[60vh]">
<div className="text-center py-8 text-gray-500">
<Loader2Icon className="w-8 h-8 animate-spin mx-auto mb-3 text-brand-600" />
<p>Loading pipeline configuration...</p>
</div>
</div>
);
}
if (!config) {
return null;
}
return (
<>
<PageMeta title="Pipeline Settings" description="Configure automation pipeline" />
<div className="space-y-6">
<PageHeader
title="Pipeline Settings"
breadcrumb="Automation / Pipeline Settings"
description="Configure 7-stage automation pipeline processing"
/>
<form onSubmit={(e) => { e.preventDefault(); saveConfig(); }}>
{/* Schedule Configuration */}
<Card className="p-6 mb-6">
<h3 className="text-lg font-semibold mb-4">Schedule Configuration</h3>
<div className="space-y-4">
{/* Enable/Disable */}
<div>
<Checkbox
label="Enable Scheduled Automation"
checked={formData.is_enabled || false}
onChange={(checked) =>
setFormData({ ...formData, is_enabled: checked })
}
/>
<p className="text-sm text-gray-600 dark:text-gray-400 ml-6">
When enabled, automation will run on the configured schedule
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Frequency */}
<div>
<label className="block font-semibold mb-1">Frequency</label>
<Select
options={[
{ value: 'daily', label: 'Daily' },
{ value: 'weekly', label: 'Weekly (Mondays)' },
{ value: 'monthly', label: 'Monthly (1st of month)' },
]}
defaultValue={formData.frequency || 'daily'}
onChange={(val) =>
setFormData({
...formData,
frequency: val as 'daily' | 'weekly' | 'monthly',
})
}
/>
</div>
{/* Scheduled Time */}
<div>
<InputField
label="Scheduled Time"
type="time"
value={formData.scheduled_time || '02:00'}
onChange={(e) =>
setFormData({ ...formData, scheduled_time: e.target.value })
}
hint="Time of day to run automation (24-hour format)"
/>
</div>
</div>
</div>
</Card>
{/* Stage Processing Toggles */}
<Card className="p-6 mb-6">
<h3 className="text-lg font-semibold mb-2">Stage Processing</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
Enable or disable individual stages. Disabled stages will be skipped during automation.
</p>
<div className="grid grid-cols-1 gap-2">
<Checkbox
label="Stage 1: Keywords → Clusters"
checked={formData.stage_1_enabled ?? true}
onChange={(checked) =>
setFormData({ ...formData, stage_1_enabled: checked })
}
/>
<Checkbox
label="Stage 2: Clusters → Ideas"
checked={formData.stage_2_enabled ?? true}
onChange={(checked) =>
setFormData({ ...formData, stage_2_enabled: checked })
}
/>
<Checkbox
label="Stage 3: Ideas → Tasks"
checked={formData.stage_3_enabled ?? true}
onChange={(checked) =>
setFormData({ ...formData, stage_3_enabled: checked })
}
/>
<Checkbox
label="Stage 4: Tasks → Content"
checked={formData.stage_4_enabled ?? true}
onChange={(checked) =>
setFormData({ ...formData, stage_4_enabled: checked })
}
/>
<Checkbox
label="Stage 5: Content → Image Prompts"
checked={formData.stage_5_enabled ?? true}
onChange={(checked) =>
setFormData({ ...formData, stage_5_enabled: checked })
}
/>
<Checkbox
label="Stage 6: Image Prompts → Images"
checked={formData.stage_6_enabled ?? true}
onChange={(checked) =>
setFormData({ ...formData, stage_6_enabled: checked })
}
/>
<Checkbox
label="Stage 7: In Review → Approved"
checked={formData.stage_7_enabled ?? true}
onChange={(checked) =>
setFormData({ ...formData, stage_7_enabled: checked })
}
/>
</div>
</Card>
{/* Batch Sizes */}
<Card className="p-6 mb-6">
<h3 className="text-lg font-semibold mb-2">Batch Sizes</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
Configure how many items to process in each stage
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<InputField
label="Stage 1: Keywords → Clusters"
type="number"
value={formData.stage_1_batch_size || 20}
onChange={(e) =>
setFormData({
...formData,
stage_1_batch_size: parseInt(e.target.value),
})
}
min="1"
max="100"
/>
<InputField
label="Stage 2: Clusters → Ideas"
type="number"
value={formData.stage_2_batch_size || 1}
onChange={(e) =>
setFormData({
...formData,
stage_2_batch_size: parseInt(e.target.value),
})
}
min="1"
max="10"
/>
<InputField
label="Stage 3: Ideas → Tasks"
type="number"
value={formData.stage_3_batch_size || 20}
onChange={(e) =>
setFormData({
...formData,
stage_3_batch_size: parseInt(e.target.value),
})
}
min="1"
max="100"
/>
<InputField
label="Stage 4: Tasks → Content"
type="number"
value={formData.stage_4_batch_size || 1}
onChange={(e) =>
setFormData({
...formData,
stage_4_batch_size: parseInt(e.target.value),
})
}
min="1"
max="10"
/>
<InputField
label="Stage 5: Content → Image Prompts"
type="number"
value={formData.stage_5_batch_size || 1}
onChange={(e) =>
setFormData({
...formData,
stage_5_batch_size: parseInt(e.target.value),
})
}
min="1"
max="10"
/>
<InputField
label="Stage 6: Image Prompts → Images"
type="number"
value={formData.stage_6_batch_size || 1}
onChange={(e) =>
setFormData({
...formData,
stage_6_batch_size: parseInt(e.target.value),
})
}
min="1"
max="10"
/>
</div>
</Card>
{/* AI Request Delays */}
<Card className="p-6 mb-6">
<h3 className="text-lg font-semibold mb-2">AI Request Delays</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
Configure delays to prevent rate limiting and manage API load
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<InputField
label="Within-Stage Delay (seconds)"
type="number"
value={formData.within_stage_delay || 3}
onChange={(e) =>
setFormData({
...formData,
within_stage_delay: parseInt(e.target.value),
})
}
min="0"
max="30"
hint="Delay between batches within a stage"
/>
<InputField
label="Between-Stage Delay (seconds)"
type="number"
value={formData.between_stage_delay || 5}
onChange={(e) =>
setFormData({
...formData,
between_stage_delay: parseInt(e.target.value),
})
}
min="0"
max="60"
hint="Delay between stage transitions"
/>
</div>
</Card>
{/* Save Button */}
<div className="flex justify-end gap-2">
<Button
type="button"
onClick={() => navigate('/automation/overview')}
variant="secondary"
>
Cancel
</Button>
<Button
type="submit"
variant="primary"
disabled={saving}
>
{saving ? 'Saving...' : 'Save Configuration'}
</Button>
</div>
</form>
</div>
</>
);
}

View File

@@ -0,0 +1,380 @@
/**
* Publisher Settings Page
* Configure automatic approval, publishing limits, and scheduling
* Uses store-based activeSite instead of URL-based siteId (BUG FIX)
*/
import React, { useState, useEffect } 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 IconButton from '../../components/ui/button/IconButton';
import Label from '../../components/form/Label';
import InputField from '../../components/form/input/InputField';
import Switch from '../../components/form/switch/Switch';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchAPI } from '../../services/api';
import { useSiteStore } from '../../store/siteStore';
import { BoltIcon, LayersIcon, CalendarIcon, InfoIcon, CloseIcon, PlusIcon, Loader2Icon } from '../../icons';
export default function PublishSettings() {
const navigate = useNavigate();
const toast = useToast();
const { activeSite } = useSiteStore(); // Use store instead of URL params
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [settings, setSettings] = useState<any>(null);
// Load publishing settings for active site
const loadSettings = async () => {
if (!activeSite) return;
try {
setLoading(true);
const response = await fetchAPI(`/v1/integration/sites/${activeSite.id}/publishing-settings/`);
setSettings(response.data || response);
} catch (error: any) {
console.error('Failed to load publishing settings:', error);
// Set defaults if endpoint fails
setSettings({
auto_approval_enabled: true,
auto_publish_enabled: true,
daily_publish_limit: 3,
weekly_publish_limit: 15,
monthly_publish_limit: 50,
publish_days: ['mon', 'tue', 'wed', 'thu', 'fri'],
publish_time_slots: ['09:00', '14:00', '18:00'],
});
} finally {
setLoading(false);
}
};
// Save publishing settings
const saveSettings = async (newSettings: any) => {
if (!activeSite) return;
try {
setSaving(true);
const response = await fetchAPI(`/v1/integration/sites/${activeSite.id}/publishing-settings/`, {
method: 'PATCH',
body: JSON.stringify(newSettings),
});
setSettings(response.data || response);
toast.success('Publishing settings saved successfully');
} catch (error: any) {
console.error('Failed to save publishing settings:', error);
toast.error('Failed to save publishing settings');
} finally {
setSaving(false);
}
};
// Load settings on mount and when active site changes
useEffect(() => {
if (activeSite) {
loadSettings();
}
}, [activeSite]);
if (!activeSite) {
return (
<div className="p-6">
<p className="text-gray-600 dark:text-gray-400">Please select a site to configure publishing settings.</p>
</div>
);
}
if (loading) {
return (
<div className="flex items-center justify-center min-h-[60vh]">
<div className="text-center py-8 text-gray-500">
<Loader2Icon className="w-8 h-8 animate-spin mx-auto mb-3 text-brand-600" />
<p>Loading publishing settings...</p>
</div>
</div>
);
}
if (!settings) {
return null;
}
return (
<>
<PageMeta title="Publishing Settings" description="Configure publishing automation and scheduling" />
<div className="space-y-6">
<PageHeader
title="Publishing Settings"
breadcrumb="Publisher / Settings"
description="Configure automatic approval, publishing limits, and scheduling"
/>
{/* 3 Cards in a Row */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Card 1: Automation */}
<Card className="p-6 border-l-4 border-l-brand-500">
<div className="flex items-center gap-3 mb-4">
<div className="p-2 bg-brand-100 dark:bg-brand-900/30 rounded-lg">
<BoltIcon className="w-5 h-5 text-brand-600 dark:text-brand-400" />
</div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Automation</h2>
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-6">
Configure automatic content approval and publishing to WordPress
</p>
<div className="space-y-6">
<div>
<div className="flex items-center justify-between mb-2">
<Label>Auto-Approval</Label>
<Switch
label=""
checked={settings.auto_approval_enabled}
onChange={(checked) => {
const newSettings = { ...settings, auto_approval_enabled: checked };
setSettings(newSettings);
saveSettings({ auto_approval_enabled: checked });
}}
/>
</div>
<p className="text-xs text-gray-500">
Automatically approve content after review
</p>
</div>
<div>
<div className="flex items-center justify-between mb-2">
<Label>Auto-Publish</Label>
<Switch
label=""
checked={settings.auto_publish_enabled}
onChange={(checked) => {
const newSettings = { ...settings, auto_publish_enabled: checked };
setSettings(newSettings);
saveSettings({ auto_publish_enabled: checked });
}}
/>
</div>
<p className="text-xs text-gray-500">
Publish approved content to WordPress
</p>
</div>
</div>
</Card>
{/* Card 2: Publishing Limits */}
<Card className="p-6 border-l-4 border-l-success-500">
<div className="flex items-center gap-3 mb-4">
<div className="p-2 bg-success-100 dark:bg-success-900/30 rounded-lg">
<LayersIcon className="w-5 h-5 text-success-600 dark:text-success-400" />
</div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Limits</h2>
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-6">
Set maximum articles to publish per day, week, and month
</p>
<div className="space-y-4">
<div>
<Label>Daily</Label>
<InputField
type="number"
min="1"
max="50"
value={settings.daily_publish_limit}
onChange={(e) => {
const value = Math.max(1, Math.min(50, parseInt(e.target.value) || 1));
setSettings({ ...settings, daily_publish_limit: value });
}}
/>
<p className="text-xs text-gray-500 mt-1">Articles per day</p>
</div>
<div>
<Label>Weekly</Label>
<InputField
type="number"
min="1"
max="200"
value={settings.weekly_publish_limit}
onChange={(e) => {
const value = Math.max(1, Math.min(200, parseInt(e.target.value) || 1));
setSettings({ ...settings, weekly_publish_limit: value });
}}
/>
<p className="text-xs text-gray-500 mt-1">Articles per week</p>
</div>
<div>
<Label>Monthly</Label>
<InputField
type="number"
min="1"
max="500"
value={settings.monthly_publish_limit}
onChange={(e) => {
const value = Math.max(1, Math.min(500, parseInt(e.target.value) || 1));
setSettings({ ...settings, monthly_publish_limit: value });
}}
/>
<p className="text-xs text-gray-500 mt-1">Articles per month</p>
</div>
</div>
</Card>
{/* Card 3: Schedule (Days + Time Slots) */}
<Card className="p-6 border-l-4 border-l-purple-500">
<div className="flex items-center gap-3 mb-4">
<div className="p-2 bg-purple-100 dark:bg-purple-900/30 rounded-lg">
<CalendarIcon className="w-5 h-5 text-purple-600 dark:text-purple-400" />
</div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Schedule</h2>
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-6">
Select which days and times to automatically publish content
</p>
<div className="space-y-6">
<div>
<Label className="mb-2">Publishing Days</Label>
<div className="flex flex-wrap gap-2">
{[
{ value: 'mon', label: 'M' },
{ value: 'tue', label: 'T' },
{ value: 'wed', label: 'W' },
{ value: 'thu', label: 'T' },
{ value: 'fri', label: 'F' },
{ value: 'sat', label: 'S' },
{ value: 'sun', label: 'S' },
].map((day) => (
<Button
key={day.value}
variant={(settings.publish_days || []).includes(day.value) ? 'primary' : 'outline'}
tone="brand"
size="sm"
onClick={() => {
const currentDays = settings.publish_days || [];
const newDays = currentDays.includes(day.value)
? currentDays.filter((d: string) => d !== day.value)
: [...currentDays, day.value];
setSettings({ ...settings, publish_days: newDays });
}}
className="w-10 h-10 p-0"
>
{day.label}
</Button>
))}
</div>
</div>
<div>
<div className="flex items-center justify-between mb-2">
<Label>Time Slots</Label>
{(settings.publish_time_slots || []).length > 0 && (
<Button
variant="ghost"
tone="danger"
size="xs"
onClick={() => {
setSettings({ ...settings, publish_time_slots: [] });
}}
>
Clear All
</Button>
)}
</div>
<p className="text-xs text-gray-500 mb-3">In your local timezone. Content will be published at these times on selected days.</p>
<div className="space-y-2">
{(settings.publish_time_slots || ['09:00', '14:00', '18:00']).length === 0 ? (
<div className="text-center py-4 text-gray-500 text-sm border border-dashed border-gray-300 dark:border-gray-700 rounded-md">
No time slots configured. Add at least one time slot.
</div>
) : (
(settings.publish_time_slots || ['09:00', '14:00', '18:00']).map((time: string, index: number) => (
<div key={index} className="flex items-center gap-2">
<span className="text-xs text-gray-500 w-8">#{index + 1}</span>
<InputField
type="time"
value={time}
onChange={(e) => {
const newSlots = [...(settings.publish_time_slots || [])];
newSlots[index] = e.target.value;
setSettings({ ...settings, publish_time_slots: newSlots });
}}
className="flex-1"
/>
<IconButton
icon={<CloseIcon className="w-4 h-4" />}
variant="ghost"
tone="danger"
size="sm"
title="Remove this time slot"
onClick={() => {
const newSlots = (settings.publish_time_slots || []).filter((_: string, i: number) => i !== index);
setSettings({ ...settings, publish_time_slots: newSlots });
}}
/>
</div>
))
)}
<Button
variant="ghost"
tone="brand"
size="sm"
startIcon={<PlusIcon className="w-4 h-4" />}
onClick={() => {
const lastSlot = (settings.publish_time_slots || [])[
(settings.publish_time_slots || []).length - 1
];
// Default new slot to 12:00 or 2 hours after last slot
let newTime = '12:00';
if (lastSlot) {
const [hours, mins] = lastSlot.split(':').map(Number);
const newHours = (hours + 2) % 24;
newTime = `${String(newHours).padStart(2, '0')}:${String(mins).padStart(2, '0')}`;
}
const newSlots = [...(settings.publish_time_slots || []), newTime];
setSettings({ ...settings, publish_time_slots: newSlots });
}}
>
Add Time Slot
</Button>
</div>
</div>
</div>
</Card>
</div>
{/* Info Box */}
<Card className="p-4 bg-brand-50 dark:bg-brand-900/20 border-brand-200 dark:border-brand-800">
<div className="flex items-start gap-3">
<InfoIcon className="w-5 h-5 text-brand-600 dark:text-brand-400 mt-0.5 flex-shrink-0" />
<div className="text-sm text-brand-800 dark:text-brand-200">
<p className="font-medium mb-1">How Publishing Works</p>
<ul className="list-disc list-inside space-y-1 text-brand-700 dark:text-brand-300">
<li>Content moves from Draft Review Approved Published</li>
<li>Auto-approval moves content from Review to Approved automatically</li>
<li>Auto-publish sends Approved content to your WordPress site</li>
<li>You can always manually publish content using the "Publish to Site" button</li>
</ul>
</div>
</div>
</Card>
{/* Save Button */}
<div className="flex justify-end">
<Button
variant="primary"
tone="brand"
onClick={() => saveSettings(settings)}
disabled={saving}
>
{saving ? 'Saving...' : 'Save Publishing Settings'}
</Button>
</div>
</div>
</>
);
}

View File

@@ -52,17 +52,12 @@ export default function SiteSettings() {
const siteSelectorRef = useRef<HTMLButtonElement>(null);
// Check for tab parameter in URL - content-types removed, redirects to integrations
const initialTab = (searchParams.get('tab') as 'general' | 'ai-settings' | 'integrations' | 'publishing') || 'general';
const [activeTab, setActiveTab] = useState<'general' | 'ai-settings' | 'integrations' | 'publishing'>(initialTab);
const initialTab = (searchParams.get('tab') as 'general' | 'ai-settings' | 'integrations') || 'general';
const [activeTab, setActiveTab] = useState<'general' | 'ai-settings' | 'integrations'>(initialTab);
// Advanced Settings toggle
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
// Publishing settings state
const [publishingSettings, setPublishingSettings] = useState<any>(null);
const [publishingSettingsLoading, setPublishingSettingsLoading] = useState(false);
const [publishingSettingsSaving, setPublishingSettingsSaving] = useState(false);
// AI Settings state (merged content generation + image settings per plan)
// Content writing settings
const [contentGenerationSettings, setContentGenerationSettings] = useState({
@@ -142,7 +137,7 @@ export default function SiteSettings() {
useEffect(() => {
// Update tab if URL parameter changes
const tab = searchParams.get('tab');
if (tab && ['general', 'ai-settings', 'integrations', 'publishing'].includes(tab)) {
if (tab && ['general', 'ai-settings', 'integrations'].includes(tab)) {
setActiveTab(tab as typeof activeTab);
}
// Handle legacy tab names - redirect content-types to integrations
@@ -250,47 +245,6 @@ export default function SiteSettings() {
}
};
const loadPublishingSettings = async () => {
if (!siteId) return;
try {
setPublishingSettingsLoading(true);
const response = await fetchAPI(`/v1/integration/sites/${siteId}/publishing-settings/`);
setPublishingSettings(response.data || response);
} catch (error: any) {
console.error('Failed to load publishing settings:', error);
// Set defaults if endpoint fails
setPublishingSettings({
auto_approval_enabled: true,
auto_publish_enabled: true,
daily_publish_limit: 3,
weekly_publish_limit: 15,
monthly_publish_limit: 50,
publish_days: ['mon', 'tue', 'wed', 'thu', 'fri'],
publish_time_slots: ['09:00', '14:00', '18:00'],
});
} finally {
setPublishingSettingsLoading(false);
}
};
const savePublishingSettings = async (newSettings: any) => {
if (!siteId) return;
try {
setPublishingSettingsSaving(true);
const response = await fetchAPI(`/v1/integration/sites/${siteId}/publishing-settings/`, {
method: 'PATCH',
body: JSON.stringify(newSettings),
});
setPublishingSettings(response.data || response);
toast.success('Publishing settings saved successfully');
} catch (error: any) {
console.error('Failed to save publishing settings:', error);
toast.error('Failed to save publishing settings');
} finally {
setPublishingSettingsSaving(false);
}
};
// Content Generation Settings
const loadContentGenerationSettings = async () => {
try {
@@ -653,21 +607,6 @@ export default function SiteSettings() {
>
Integrations
</Button>
<Button
variant="ghost"
onClick={() => {
setActiveTab('publishing');
navigate(`/sites/${siteId}/settings?tab=publishing`, { replace: true });
}}
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors whitespace-nowrap ${
activeTab === 'publishing'
? 'border-info-500 text-info-600 dark:text-info-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
}`}
startIcon={<PaperPlaneIcon className={`w-4 h-4 ${activeTab === 'publishing' ? 'text-info-500' : ''}`} />}
>
Publishing
</Button>
</div>
</div>
</div>
@@ -922,306 +861,6 @@ export default function SiteSettings() {
</div>
)}
{/* Publishing Tab */}
{activeTab === 'publishing' && (
<div className="space-y-6">
{publishingSettingsLoading ? (
<Card>
<div className="p-6">
<div className="text-center py-8 text-gray-500">
<Loader2Icon className="w-8 h-8 animate-spin mx-auto mb-3 text-brand-600" />
<p>Loading publishing settings...</p>
</div>
</div>
</Card>
) : publishingSettings ? (
<>
{/* 3 Cards in a Row */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Card 1: Automation */}
<Card className="p-6 border-l-4 border-l-brand-500">
<div className="flex items-center gap-3 mb-4">
<div className="p-2 bg-brand-100 dark:bg-brand-900/30 rounded-lg">
<BoltIcon className="w-5 h-5 text-brand-600 dark:text-brand-400" />
</div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Automation</h2>
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-6">
Configure automatic content approval and publishing to WordPress
</p>
<div className="space-y-6">
<div>
<div className="flex items-center justify-between mb-2">
<Label>Auto-Approval</Label>
<Switch
label=""
checked={publishingSettings.auto_approval_enabled}
onChange={(checked) => {
const newSettings = { ...publishingSettings, auto_approval_enabled: checked };
setPublishingSettings(newSettings);
savePublishingSettings({ auto_approval_enabled: checked });
}}
/>
</div>
<p className="text-xs text-gray-500">
Automatically approve content after review
</p>
</div>
<div>
<div className="flex items-center justify-between mb-2">
<Label>Auto-Publish</Label>
<Switch
label=""
checked={publishingSettings.auto_publish_enabled}
onChange={(checked) => {
const newSettings = { ...publishingSettings, auto_publish_enabled: checked };
setPublishingSettings(newSettings);
savePublishingSettings({ auto_publish_enabled: checked });
}}
/>
</div>
<p className="text-xs text-gray-500">
Publish approved content to WordPress
</p>
</div>
</div>
</Card>
{/* Card 2: Publishing Limits */}
<Card className="p-6 border-l-4 border-l-success-500">
<div className="flex items-center gap-3 mb-4">
<div className="p-2 bg-success-100 dark:bg-success-900/30 rounded-lg">
<LayersIcon className="w-5 h-5 text-success-600 dark:text-success-400" />
</div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Limits</h2>
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-6">
Set maximum articles to publish per day, week, and month
</p>
<div className="space-y-4">
<div>
<Label>Daily</Label>
<InputField
type="number"
min="1"
max="50"
value={publishingSettings.daily_publish_limit}
onChange={(e) => {
const value = Math.max(1, Math.min(50, parseInt(e.target.value) || 1));
setPublishingSettings({ ...publishingSettings, daily_publish_limit: value });
}}
/>
<p className="text-xs text-gray-500 mt-1">Articles per day</p>
</div>
<div>
<Label>Weekly</Label>
<InputField
type="number"
min="1"
max="200"
value={publishingSettings.weekly_publish_limit}
onChange={(e) => {
const value = Math.max(1, Math.min(200, parseInt(e.target.value) || 1));
setPublishingSettings({ ...publishingSettings, weekly_publish_limit: value });
}}
/>
<p className="text-xs text-gray-500 mt-1">Articles per week</p>
</div>
<div>
<Label>Monthly</Label>
<InputField
type="number"
min="1"
max="500"
value={publishingSettings.monthly_publish_limit}
onChange={(e) => {
const value = Math.max(1, Math.min(500, parseInt(e.target.value) || 1));
setPublishingSettings({ ...publishingSettings, monthly_publish_limit: value });
}}
/>
<p className="text-xs text-gray-500 mt-1">Articles per month</p>
</div>
</div>
</Card>
{/* Card 3: Schedule (Days + Time Slots) */}
<Card className="p-6 border-l-4 border-l-purple-500">
<div className="flex items-center gap-3 mb-4">
<div className="p-2 bg-purple-100 dark:bg-purple-900/30 rounded-lg">
<CalendarIcon className="w-5 h-5 text-purple-600 dark:text-purple-400" />
</div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Schedule</h2>
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-6">
Select which days and times to automatically publish content
</p>
<div className="space-y-6">
<div>
<Label className="mb-2">Publishing Days</Label>
<div className="flex flex-wrap gap-2">
{[
{ value: 'mon', label: 'M' },
{ value: 'tue', label: 'T' },
{ value: 'wed', label: 'W' },
{ value: 'thu', label: 'T' },
{ value: 'fri', label: 'F' },
{ value: 'sat', label: 'S' },
{ value: 'sun', label: 'S' },
].map((day) => (
<Button
key={day.value}
variant={(publishingSettings.publish_days || []).includes(day.value) ? 'primary' : 'outline'}
tone="brand"
size="sm"
onClick={() => {
const currentDays = publishingSettings.publish_days || [];
const newDays = currentDays.includes(day.value)
? currentDays.filter((d: string) => d !== day.value)
: [...currentDays, day.value];
setPublishingSettings({ ...publishingSettings, publish_days: newDays });
// Don't auto-save - let user click Save button
}}
className="w-10 h-10 p-0"
>
{day.label}
</Button>
))}
</div>
</div>
<div>
<div className="flex items-center justify-between mb-2">
<Label>Time Slots</Label>
{(publishingSettings.publish_time_slots || []).length > 0 && (
<Button
variant="ghost"
tone="danger"
size="xs"
onClick={() => {
setPublishingSettings({ ...publishingSettings, publish_time_slots: [] });
}}
>
Clear All
</Button>
)}
</div>
<p className="text-xs text-gray-500 mb-3">In your local timezone. Content will be published at these times on selected days.</p>
<div className="space-y-2">
{(publishingSettings.publish_time_slots || ['09:00', '14:00', '18:00']).length === 0 ? (
<div className="text-center py-4 text-gray-500 text-sm border border-dashed border-gray-300 dark:border-gray-700 rounded-md">
No time slots configured. Add at least one time slot.
</div>
) : (
(publishingSettings.publish_time_slots || ['09:00', '14:00', '18:00']).map((time: string, index: number) => (
<div key={index} className="flex items-center gap-2">
<span className="text-xs text-gray-500 w-8">#{index + 1}</span>
<InputField
type="time"
value={time}
onChange={(e) => {
const newSlots = [...(publishingSettings.publish_time_slots || [])];
newSlots[index] = e.target.value;
setPublishingSettings({ ...publishingSettings, publish_time_slots: newSlots });
}}
className="flex-1"
/>
<IconButton
icon={<CloseIcon className="w-4 h-4" />}
variant="ghost"
tone="danger"
size="sm"
title="Remove this time slot"
onClick={() => {
const newSlots = (publishingSettings.publish_time_slots || []).filter((_: string, i: number) => i !== index);
setPublishingSettings({ ...publishingSettings, publish_time_slots: newSlots });
// Don't auto-save - let user click Save button
}}
/>
</div>
))
)}
<Button
variant="ghost"
tone="brand"
size="sm"
startIcon={<PlusIcon className="w-4 h-4" />}
onClick={() => {
const lastSlot = (publishingSettings.publish_time_slots || [])[
(publishingSettings.publish_time_slots || []).length - 1
];
// Default new slot to 12:00 or 2 hours after last slot
let newTime = '12:00';
if (lastSlot) {
const [hours, mins] = lastSlot.split(':').map(Number);
const newHours = (hours + 2) % 24;
newTime = `${String(newHours).padStart(2, '0')}:${String(mins).padStart(2, '0')}`;
}
const newSlots = [...(publishingSettings.publish_time_slots || []), newTime];
setPublishingSettings({ ...publishingSettings, publish_time_slots: newSlots });
// Don't auto-save - let user click Save button
}}
>
Add Time Slot
</Button>
</div>
</div>
</div>
</Card>
</div>
{/* Info Box */}
<Card className="p-4 bg-brand-50 dark:bg-brand-900/20 border-brand-200 dark:border-brand-800">
<div className="flex items-start gap-3">
<InfoIcon className="w-5 h-5 text-brand-600 dark:text-brand-400 mt-0.5 flex-shrink-0" />
<div className="text-sm text-brand-800 dark:text-brand-200">
<p className="font-medium mb-1">How Publishing Works</p>
<ul className="list-disc list-inside space-y-1 text-brand-700 dark:text-brand-300">
<li>Content moves from Draft Review Approved Published</li>
<li>Auto-approval moves content from Review to Approved automatically</li>
<li>Auto-publish sends Approved content to your WordPress site</li>
<li>You can always manually publish content using the "Publish to Site" button</li>
</ul>
</div>
</div>
</Card>
{/* Save Button */}
<div className="flex justify-end">
<Button
variant="primary"
tone="brand"
onClick={() => savePublishingSettings(publishingSettings)}
isLoading={publishingSettingsSaving}
disabled={publishingSettingsSaving}
>
Save Publishing Settings
</Button>
</div>
</>
) : (
<Card>
<div className="p-6">
<div className="text-center py-8 text-gray-500">
<p>Failed to load publishing settings. Please try again.</p>
<Button
variant="outline"
className="mt-4"
onClick={loadPublishingSettings}
>
Retry
</Button>
</div>
</div>
</Card>
)}
</div>
)}
{/* Tab content */}
<div className="space-y-6">
{/* General Tab */}

View File

@@ -720,9 +720,9 @@ export default function Approved() {
return (
<>
<PageHeader
title="Content Approved"
badge={{ icon: <CheckCircleIcon />, color: 'green' }}
parent="Writer"
title="Publish / Schedule"
badge={{ icon: <RocketLaunchIcon />, color: 'green' }}
parent="Publisher"
/>
<TablePageTemplate
columns={pageConfig.columns}

View File

@@ -391,7 +391,7 @@ export default function Review() {
<PageHeader
title="Content Review"
badge={{ icon: <ClipboardDocumentCheckIcon />, color: 'emerald' }}
parent="Writer"
parent="Publisher"
/>
<TablePageTemplate
columns={pageConfig.columns}

View File

@@ -1,13 +1,11 @@
/**
* Account Settings Page - Consolidated Settings
* Tabs: Account, Profile, Team
* Tab selection driven by URL path for sidebar navigation
* Account Settings Page - Single Scrollable Page
* All settings displayed vertically: Account, Profile, Team
*/
import { useState, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import {
SaveIcon, Loader2Icon, SettingsIcon, UserIcon, UsersIcon, UserPlusIcon, LockIcon, LockIcon as ShieldIcon, XIcon
SaveIcon, Loader2Icon, SettingsIcon, UserIcon, UsersIcon, UserPlusIcon, LockIcon, LockIcon as ShieldIcon
} from '../../icons';
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
@@ -34,23 +32,12 @@ import {
type TeamMember,
} from '../../services/billing.api';
type TabType = 'account' | 'profile' | 'team';
// Map URL paths to tab types
function getTabFromPath(pathname: string): TabType {
if (pathname.includes('/profile')) return 'profile';
if (pathname.includes('/team')) return 'team';
return 'account';
}
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 [saving, setSaving] = useState(false);
const [savingAccount, setSavingAccount] = useState(false);
const [savingProfile, setSavingProfile] = useState(false);
const [error, setError] = useState<string>('');
const [success, setSuccess] = useState<string>('');
@@ -100,10 +87,6 @@ export default function AccountSettingsPage() {
last_name: '',
});
useEffect(() => {
loadData();
}, []);
// Load profile from auth store user data
useEffect(() => {
if (user) {
@@ -121,6 +104,11 @@ export default function AccountSettingsPage() {
}
}, [user]);
useEffect(() => {
loadData();
loadTeamMembers();
}, []);
const loadData = async () => {
try {
startLoading('Loading settings...');
@@ -156,17 +144,10 @@ export default function AccountSettingsPage() {
}
};
// Load team members when team tab is selected
useEffect(() => {
if (activeTab === 'team' && members.length === 0) {
loadTeamMembers();
}
}, [activeTab]);
const handleAccountSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
setSaving(true);
setSavingAccount(true);
setError('');
setSuccess('');
await updateAccountSettings(accountForm);
@@ -177,14 +158,14 @@ export default function AccountSettingsPage() {
setError(err.message || 'Failed to update account settings');
toast.error(err.message || 'Failed to save settings');
} finally {
setSaving(false);
setSavingAccount(false);
}
};
const handleProfileSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
setSaving(true);
setSavingProfile(true);
// Profile data is stored in auth user - refresh after save
// Note: Full profile API would go here when backend supports it
toast.success('Profile settings saved');
@@ -192,7 +173,7 @@ export default function AccountSettingsPage() {
} catch (err: any) {
toast.error(err.message || 'Failed to save profile');
} finally {
setSaving(false);
setSavingProfile(false);
}
};
@@ -264,68 +245,42 @@ export default function AccountSettingsPage() {
}));
};
const tabs = [
{ id: 'account' as TabType, label: 'Account', icon: <SettingsIcon className="w-4 h-4" /> },
{ id: 'profile' as TabType, label: 'Profile', icon: <UserIcon className="w-4 h-4" /> },
{ id: 'team' as TabType, label: 'Team', icon: <UsersIcon className="w-4 h-4" /> },
];
// Page titles based on active tab
const pageTitles = {
account: { title: 'Account Information', description: 'Manage your organization and billing information' },
profile: { title: 'Profile Settings', description: 'Update your personal information and preferences' },
team: { title: 'Team Management', description: 'Invite and manage team members' },
};
return (
<>
<PageMeta title="Account Settings" description="Manage your account, profile, and team" />
<PageHeader
title={pageTitles[activeTab].title}
description={pageTitles[activeTab].description}
title="Account Settings"
description="Manage your account information, profile, and team"
badge={{ icon: <SettingsIcon className="w-4 h-4" />, color: 'blue' }}
parent="Account Settings"
/>
{/* Tab Content */}
{/* Account Tab */}
{activeTab === 'account' && (
<div className="space-y-6 max-w-4xl">
{error && (
<div className="p-4 bg-error-50 dark:bg-error-900/20 border border-error-200 dark:border-error-800 rounded-lg">
<p className="text-error-800 dark:text-error-200">{error}</p>
</div>
)}
{success && (
<div className="p-4 bg-success-50 dark:bg-success-900/20 border border-success-200 dark:border-success-800 rounded-lg">
<p className="text-success-800 dark:text-success-200">{success}</p>
</div>
)}
<form onSubmit={handleAccountSubmit} className="space-y-6">
{/* Account Information */}
<div className="space-y-8">
{/* Account Settings Section */}
<div className="space-y-4">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<SettingsIcon className="w-5 h-5" />
Account Information
</h2>
<form onSubmit={handleAccountSubmit}>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Account Information Card */}
<Card className="p-6 border-l-4 border-l-brand-500">
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Account Information</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<InputField
type="text"
name="name"
label="Account Name"
value={accountForm.name}
onChange={handleAccountChange}
/>
</div>
<div>
<InputField
type="text"
label="Account Slug"
value={settings?.slug || ''}
disabled
/>
</div>
</div>
<div className="mt-4">
<h3 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Account Details</h3>
<div className="space-y-4">
<InputField
type="text"
name="name"
label="Account Name"
value={accountForm.name}
onChange={handleAccountChange}
/>
<InputField
type="text"
label="Account Slug"
value={settings?.slug || ''}
disabled
/>
<InputField
type="email"
name="billing_email"
@@ -336,74 +291,64 @@ export default function AccountSettingsPage() {
</div>
</Card>
{/* Billing Address */}
{/* Billing Address Card */}
<Card className="p-6 border-l-4 border-l-purple-500">
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Billing Address</h2>
<h3 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Billing Address</h3>
<div className="space-y-4">
<div>
<InputField
type="text"
name="billing_address_line1"
label="Address Line 1"
value={accountForm.billing_address_line1}
onChange={handleAccountChange}
/>
<InputField
type="text"
name="billing_address_line2"
label="Address Line 2"
value={accountForm.billing_address_line2}
onChange={handleAccountChange}
placeholder="Apartment, suite, etc. (optional)"
/>
<div className="grid grid-cols-2 gap-4">
<InputField
type="text"
name="billing_address_line1"
label="Address Line 1"
value={accountForm.billing_address_line1}
name="billing_city"
label="City"
value={accountForm.billing_city}
onChange={handleAccountChange}
/>
<InputField
type="text"
name="billing_state"
label="State"
value={accountForm.billing_state}
onChange={handleAccountChange}
/>
</div>
<div>
<div className="grid grid-cols-2 gap-4">
<InputField
type="text"
name="billing_address_line2"
label="Address Line 2"
value={accountForm.billing_address_line2}
name="billing_postal_code"
label="Postal Code"
value={accountForm.billing_postal_code}
onChange={handleAccountChange}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<InputField
type="text"
name="billing_city"
label="City"
value={accountForm.billing_city}
onChange={handleAccountChange}
/>
</div>
<div>
<InputField
type="text"
name="billing_state"
label="State/Province"
value={accountForm.billing_state}
onChange={handleAccountChange}
/>
</div>
<div>
<InputField
type="text"
name="billing_postal_code"
label="Postal Code"
value={accountForm.billing_postal_code}
onChange={handleAccountChange}
/>
</div>
</div>
<div>
<InputField
type="text"
name="billing_country"
label="Country"
value={accountForm.billing_country}
onChange={handleAccountChange}
placeholder="US, GB, IN, etc."
/>
</div>
</div>
</Card>
{/* Tax Information */}
{/* Tax Information Card */}
<Card className="p-6 border-l-4 border-l-success-500">
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Tax Information</h2>
<div>
<h3 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Tax Information</h3>
<div className="space-y-4">
<InputField
type="text"
name="tax_id"
@@ -412,70 +357,73 @@ export default function AccountSettingsPage() {
onChange={handleAccountChange}
placeholder="Optional"
/>
<div className="text-sm text-gray-600 dark:text-gray-400 mt-4">
<p className="font-medium mb-2">Tax Information</p>
<p>Provide your tax ID or VAT number if applicable for your region.</p>
</div>
</div>
</Card>
</div>
{/* Submit Button */}
<div className="flex justify-end">
<Button
type="submit"
variant="primary"
tone="brand"
disabled={saving}
startIcon={saving ? <Loader2Icon className="w-4 h-4 animate-spin" /> : <SaveIcon className="w-4 h-4" />}
>
{saving ? 'Saving...' : 'Save Changes'}
</Button>
</div>
</form>
</div>
)}
{/* Save Button for Account Section */}
<div className="flex justify-end mt-6">
<Button
type="submit"
variant="primary"
tone="brand"
disabled={savingAccount}
startIcon={savingAccount ? <Loader2Icon className="w-4 h-4 animate-spin" /> : <SaveIcon className="w-4 h-4" />}
>
{savingAccount ? 'Saving...' : 'Save Account Settings'}
</Button>
</div>
</form>
</div>
{/* Profile Tab */}
{activeTab === 'profile' && (
<div className="space-y-6 max-w-4xl">
<form onSubmit={handleProfileSubmit} className="space-y-6">
{/* Profile Settings Section */}
<div className="space-y-4 pt-8 border-t border-gray-200 dark:border-gray-700">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<UserIcon className="w-5 h-5" />
Profile Settings
</h2>
<form onSubmit={handleProfileSubmit}>
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* About You Card */}
<Card className="p-6 border-l-4 border-l-brand-500">
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">About You</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<InputField
type="text"
label="First Name"
value={profileForm.firstName}
onChange={(e) => setProfileForm({ ...profileForm, firstName: e.target.value })}
/>
</div>
<div>
<InputField
type="text"
label="Last Name"
value={profileForm.lastName}
onChange={(e) => setProfileForm({ ...profileForm, lastName: e.target.value })}
/>
</div>
<div>
<InputField
type="email"
label="Email"
value={profileForm.email}
onChange={(e) => setProfileForm({ ...profileForm, email: e.target.value })}
/>
</div>
<div>
<InputField
type="tel"
label="Phone Number (optional)"
value={profileForm.phone}
onChange={(e) => setProfileForm({ ...profileForm, phone: e.target.value })}
/>
</div>
<h3 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">About You</h3>
<div className="space-y-4">
<InputField
type="text"
label="First Name"
value={profileForm.firstName}
onChange={(e) => setProfileForm({ ...profileForm, firstName: e.target.value })}
/>
<InputField
type="text"
label="Last Name"
value={profileForm.lastName}
onChange={(e) => setProfileForm({ ...profileForm, lastName: e.target.value })}
/>
<InputField
type="email"
label="Email"
value={profileForm.email}
onChange={(e) => setProfileForm({ ...profileForm, email: e.target.value })}
/>
<InputField
type="tel"
label="Phone Number (optional)"
value={profileForm.phone}
onChange={(e) => setProfileForm({ ...profileForm, phone: e.target.value })}
/>
</div>
</Card>
{/* Preferences Card */}
<Card className="p-6 border-l-4 border-l-purple-500">
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Preferences</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<h3 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Preferences</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Your Timezone
@@ -511,88 +459,98 @@ export default function AccountSettingsPage() {
</div>
</Card>
{/* Notifications Card */}
<Card className="p-6 border-l-4 border-l-success-500">
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Notifications</h2>
<h3 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Notifications</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
Choose what emails you want to receive:
</p>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-gray-900 dark:text-white">Important Updates</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
Get notified about important changes to your account
</div>
</div>
<div className="flex items-start gap-3">
<Checkbox
checked={profileForm.emailNotifications}
onChange={(checked) => setProfileForm({ ...profileForm, emailNotifications: checked })}
/>
</div>
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-gray-900 dark:text-white">Tips & Product Updates</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
Hear about new features and content tips
<div className="font-medium text-gray-900 dark:text-white text-sm">Important Updates</div>
<div className="text-xs text-gray-600 dark:text-gray-400">
Get notified about important changes
</div>
</div>
</div>
<div className="flex items-start gap-3">
<Checkbox
checked={profileForm.marketingEmails}
onChange={(checked) => setProfileForm({ ...profileForm, marketingEmails: checked })}
/>
<div>
<div className="font-medium text-gray-900 dark:text-white text-sm">Tips & Updates</div>
<div className="text-xs text-gray-600 dark:text-gray-400">
Hear about new features and tips
</div>
</div>
</div>
</div>
</Card>
{/* Security Card */}
<Card className="p-6 border-l-4 border-l-warning-500">
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white flex items-center gap-2">
<h3 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white flex items-center gap-2">
<LockIcon className="w-5 h-5" />
Security
</h2>
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
Keep your account secure by updating your password regularly.
</p>
<Button
variant="outline"
tone="neutral"
onClick={() => setShowPasswordModal(true)}
type="button"
>
Change Password
</Button>
</Card>
</div>
{/* Submit Button */}
<div className="flex justify-end">
<Button
type="submit"
variant="primary"
tone="brand"
disabled={saving}
startIcon={saving ? <Loader2Icon className="w-4 h-4 animate-spin" /> : <SaveIcon className="w-4 h-4" />}
>
{saving ? 'Saving...' : 'Save Profile'}
</Button>
</div>
</form>
</div>
)}
{/* Save Button for Profile Section */}
<div className="flex justify-end mt-6">
<Button
type="submit"
variant="primary"
tone="brand"
disabled={savingProfile}
startIcon={savingProfile ? <Loader2Icon className="w-4 h-4 animate-spin" /> : <SaveIcon className="w-4 h-4" />}
>
{savingProfile ? 'Saving...' : 'Save Profile Settings'}
</Button>
</div>
</form>
</div>
{/* Team Tab */}
{activeTab === 'team' && (
<div className="space-y-6">
{/* Team Members Section */}
<ComponentCard
title="Team Members"
desc="Manage who can access your account"
headerContent={
<Button
variant="primary"
tone="brand"
size="sm"
startIcon={<UserPlusIcon className="w-4 h-4" />}
onClick={() => setShowInviteModal(true)}
>
Invite Someone
</Button>
}
>
{/* Team Management Section */}
<div className="space-y-4 pt-8 border-t border-gray-200 dark:border-gray-700">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<UsersIcon className="w-5 h-5" />
Team Management
</h2>
{/* Team Members Section */}
<ComponentCard
title="Team Members"
desc="Manage who can access your account"
headerContent={
<Button
variant="primary"
tone="brand"
size="sm"
startIcon={<UserPlusIcon className="w-4 h-4" />}
onClick={() => setShowInviteModal(true)}
>
Invite Someone
</Button>
}
>
{teamLoading ? (
<div className="flex items-center justify-center h-32">
<Loader2Icon className="w-6 h-6 animate-spin text-[var(--color-brand-500)]" />
@@ -696,10 +654,10 @@ export default function AccountSettingsPage() {
</div>
</ComponentCard>
</div>
)}
</div>
{/* Invite Modal */}
<Modal
{/* Invite Modal */}
<Modal
isOpen={showInviteModal}
onClose={() => {
setShowInviteModal(false);

File diff suppressed because it is too large Load Diff

View File

@@ -460,7 +460,9 @@
----------------------------------------------------------------- */
@utility menu-item {
@apply relative flex items-center w-full gap-3 px-3 py-1.5 font-medium rounded-lg text-theme-sm;
@apply relative flex items-center w-full gap-3 px-3 font-medium rounded-lg text-theme-sm;
padding-top: 7px;
padding-bottom: 7px;
}
@utility menu-item-active {