From 4d6ee2140857291aed6284c5d6e2a839b5234d32 Mon Sep 17 00:00:00 2001 From: "IGNY8 VPS (Salman)" Date: Sat, 3 Jan 2026 08:11:41 +0000 Subject: [PATCH] Section 2 Part 3 --- .../src/components/common/ComponentCard.tsx | 27 +- .../components/common/SingleSiteSelector.tsx | 14 +- .../src/components/common/SiteInfoBar.tsx | 133 ++++ frontend/src/icons/index.ts | 1 + frontend/src/layout/AppHeader.tsx | 1 + frontend/src/layout/AppSidebar.tsx | 42 +- .../src/pages/Automation/AutomationPage.tsx | 120 +-- .../src/pages/Publisher/ContentCalendar.tsx | 241 ++++-- frontend/src/pages/Sites/Content.tsx | 45 +- frontend/src/pages/Sites/Dashboard.tsx | 324 ++++---- frontend/src/pages/Sites/Settings.tsx | 751 +++++++++--------- .../src/pages/account/AccountSettingsPage.tsx | 343 ++++---- .../src/pages/account/ContentSettingsPage.tsx | 6 +- .../src/pages/account/PlansAndBillingPage.tsx | 16 +- .../src/pages/account/UsageAnalyticsPage.tsx | 40 +- 15 files changed, 1209 insertions(+), 895 deletions(-) create mode 100644 frontend/src/components/common/SiteInfoBar.tsx diff --git a/frontend/src/components/common/ComponentCard.tsx b/frontend/src/components/common/ComponentCard.tsx index 7ed069d6..07cad9f3 100644 --- a/frontend/src/components/common/ComponentCard.tsx +++ b/frontend/src/components/common/ComponentCard.tsx @@ -3,6 +3,7 @@ interface ComponentCardProps { children: React.ReactNode; className?: string; // Additional custom classes for styling desc?: string | React.ReactNode; // Description text + headerContent?: React.ReactNode; // Additional content to display in header (e.g., actions, navigation) } const ComponentCard: React.FC = ({ @@ -10,21 +11,29 @@ const ComponentCard: React.FC = ({ children, className = "", desc = "", + headerContent, }) => { return (
{/* Card Header (render only when title or desc provided) */} - {(title || desc) && ( -
-

- {title} -

- {desc && ( -

- {desc} -

+ {(title || desc || headerContent) && ( +
+
+

+ {title} +

+ {desc && ( +

+ {desc} +

+ )} +
+ {headerContent && ( +
+ {headerContent} +
)}
)} diff --git a/frontend/src/components/common/SingleSiteSelector.tsx b/frontend/src/components/common/SingleSiteSelector.tsx index c0c2f692..2ec7c83b 100644 --- a/frontend/src/components/common/SingleSiteSelector.tsx +++ b/frontend/src/components/common/SingleSiteSelector.tsx @@ -56,13 +56,21 @@ export default function SingleSiteSelector() { } }; - const handleSiteSelect = async (siteId: number) => { + const handleSiteSelect = async (newSiteId: number) => { try { - await apiSetActiveSite(siteId); - const selectedSite = sites.find(s => s.id === siteId); + await apiSetActiveSite(newSiteId); + const selectedSite = sites.find(s => s.id === newSiteId); if (selectedSite) { setActiveSite(selectedSite); toast.success(`Switched to "${selectedSite.name}"`); + + // If we're on a site-specific page (/sites/:id/...), navigate to same subpage for new site + const path = window.location.pathname; + const sitePageMatch = path.match(/^\/sites\/(\d+)(\/.*)?$/); + if (sitePageMatch) { + const subPath = sitePageMatch[2] || ''; // e.g., '/settings', '/content', '' + navigate(`/sites/${newSiteId}${subPath}`); + } } setSitesOpen(false); } catch (error: any) { diff --git a/frontend/src/components/common/SiteInfoBar.tsx b/frontend/src/components/common/SiteInfoBar.tsx new file mode 100644 index 00000000..cd3e6baf --- /dev/null +++ b/frontend/src/components/common/SiteInfoBar.tsx @@ -0,0 +1,133 @@ +/** + * SiteInfoBar - Reusable site info bar for site-specific pages + * Shows site name, URL, badges, and action buttons in a single row + */ +import { useNavigate } from 'react-router-dom'; +import Button from '../ui/button/Button'; +import { + GridIcon, + FileIcon, + GlobeIcon, + PlusIcon, +} from '../../icons'; + +interface SiteInfoBarProps { + site: { + id: number; + name: string; + domain?: string; + url?: string; + site_type?: string; + hosting_type?: string; + is_active?: boolean; + } | null; + /** Current page - determines which buttons to show */ + currentPage: 'dashboard' | 'settings' | 'content'; + /** Optional: total items count for content page */ + itemsCount?: number; + /** Optional: show New Post button */ + showNewPostButton?: boolean; +} + +export default function SiteInfoBar({ + site, + currentPage, + itemsCount, + showNewPostButton = false, +}: SiteInfoBarProps) { + const navigate = useNavigate(); + + if (!site) return null; + + const siteUrl = site.domain || site.url; + + return ( +
+
+ {/* Left: Badges */} +
+ + {site.site_type || 'marketing'} + + + {site.hosting_type || 'igny8_sites'} + + + {site.is_active !== false ? '● Active' : '○ Inactive'} + +
+ + {/* Center: Site Name and URL */} +
+

{site.name}

+ {siteUrl && ( + + {siteUrl} + + )} + {itemsCount !== undefined && ( + + ({itemsCount} items) + + )} +
+ + {/* Right: Action Buttons */} +
+ {currentPage !== 'dashboard' && ( + + )} + {currentPage !== 'settings' && ( + + )} + {currentPage !== 'content' && ( + + )} + {showNewPostButton && ( + + )} +
+
+
+ ); +} diff --git a/frontend/src/icons/index.ts b/frontend/src/icons/index.ts index 5b617c6a..94c67d14 100644 --- a/frontend/src/icons/index.ts +++ b/frontend/src/icons/index.ts @@ -170,3 +170,4 @@ export { AlertIcon as BellIcon }; // Bell notification alias export { PlugInIcon as TestTubeIcon }; // Test tube alias export { MailIcon as Mail }; // Mail without Icon suffix export { BoxIcon as PackageIcon }; // Package alias +export { GridIcon as LayersIcon }; // Layers alias diff --git a/frontend/src/layout/AppHeader.tsx b/frontend/src/layout/AppHeader.tsx index 00eb4d43..ae7c5310 100644 --- a/frontend/src/layout/AppHeader.tsx +++ b/frontend/src/layout/AppHeader.tsx @@ -24,6 +24,7 @@ const SINGLE_SITE_ROUTES = [ '/automation', '/publisher', // Content Calendar page '/account/content-settings', // Content settings and sub-pages + '/sites', // Site dashboard and site settings pages (matches /sites/21, /sites/21/settings, /sites/21/content) ]; const SITE_WITH_ALL_SITES_ROUTES = [ diff --git a/frontend/src/layout/AppSidebar.tsx b/frontend/src/layout/AppSidebar.tsx index 1935f98a..53a5a4de 100644 --- a/frontend/src/layout/AppSidebar.tsx +++ b/frontend/src/layout/AppSidebar.tsx @@ -54,16 +54,31 @@ const AppSidebar: React.FC = () => { ); const subMenuRefs = useRef>({}); + // Check if a path is active - exact match only for menu items + // Prefix matching is only used for parent menus to determine if submenu should be open const isActive = useCallback( - (path: string) => { - // Exact match + (path: string, exactOnly: boolean = false) => { + // Exact match always works if (location.pathname === path) return true; - // For sub-pages, match if pathname starts with the path (except for root) - if (path !== '/' && location.pathname.startsWith(path + '/')) return true; + + // For prefix matching (used by parent menus to check if any child is active) + // Skip if exactOnly is requested (for submenu items) + if (!exactOnly && path !== '/' && location.pathname.startsWith(path + '/')) { + return true; + } + return false; }, [location.pathname] ); + + // Check if a submenu item path is active - uses exact match only + const isSubItemActive = useCallback( + (path: string) => { + return location.pathname === path; + }, + [location.pathname] + ); // Define menu sections with useMemo to prevent recreation on every render // New structure: Dashboard (standalone) → SETUP → WORKFLOW → SETTINGS @@ -242,15 +257,12 @@ const AppSidebar: React.FC = () => { const currentPath = location.pathname; let foundMatch = false; - // Find the matching submenu for the current path + // Find the matching submenu for the current path - use exact match only for subitems allSections.forEach((section, sectionIndex) => { section.items.forEach((nav, itemIndex) => { if (nav.subItems && !foundMatch) { - const shouldOpen = nav.subItems.some((subItem) => { - if (currentPath === subItem.path) return true; - if (subItem.path !== '/' && currentPath.startsWith(subItem.path + '/')) return true; - return false; - }); + // Only use exact match for submenu items to prevent multiple active states + const shouldOpen = nav.subItems.some((subItem) => currentPath === subItem.path); if (shouldOpen) { setOpenSubmenu((prev) => { @@ -326,8 +338,8 @@ const AppSidebar: React.FC = () => { return true; }) .map((nav, itemIndex) => { - // Check if any subitem is active to determine parent active state - const hasActiveSubItem = nav.subItems?.some(subItem => isActive(subItem.path)) ?? false; + // Check if any subitem is active to determine parent active state (uses exact match for subitems) + const hasActiveSubItem = nav.subItems?.some(subItem => isSubItemActive(subItem.path)) ?? false; const isSubmenuOpen = openSubmenu?.sectionIndex === sectionIndex && openSubmenu?.itemIndex === itemIndex; return ( @@ -408,7 +420,7 @@ const AppSidebar: React.FC = () => { { {subItem.new && ( { {subItem.pro && ( { {/* Compact Schedule & Controls Panel */} {config && ( - +
@@ -554,20 +554,20 @@ const AutomationPage: React.FC = () => { {/* Metrics Summary Cards */}
{/* Keywords */} -
+
-
- +
+
-
Keywords
+
Keywords
{(() => { const res = getStageResult(1); const total = res?.total ?? pipelineOverview[0]?.counts?.total ?? metrics?.keywords?.total ?? pipelineOverview[0]?.pending ?? 0; return (
-
{total}
+
{total}
); })()} @@ -578,28 +578,28 @@ const AutomationPage: React.FC = () => { const mapped = res?.mapped ?? pipelineOverview[0]?.counts?.mapped ?? metrics?.keywords?.mapped ?? 0; return ( renderMetricRow([ - { label: 'New:', value: newCount, colorCls: 'text-brand-700' }, - { label: 'Mapped:', value: mapped, colorCls: 'text-brand-700' }, + { label: 'New:', value: newCount, colorCls: 'text-brand-600' }, + { label: 'Mapped:', value: mapped, colorCls: 'text-brand-600' }, ]) ); })()}
{/* Clusters */} -
+
-
- +
+
-
Clusters
+
Clusters
{(() => { const res = getStageResult(2); const total = res?.total ?? pipelineOverview[1]?.counts?.total ?? metrics?.clusters?.total ?? pipelineOverview[1]?.pending ?? 0; return (
-
{total}
+
{total}
); })()} @@ -610,28 +610,28 @@ const AutomationPage: React.FC = () => { const mapped = res?.mapped ?? pipelineOverview[1]?.counts?.mapped ?? metrics?.clusters?.mapped ?? 0; return ( renderMetricRow([ - { label: 'New:', value: newCount, colorCls: 'text-purple-700' }, - { label: 'Mapped:', value: mapped, colorCls: 'text-purple-700' }, + { label: 'New:', value: newCount, colorCls: 'text-purple-600' }, + { label: 'Mapped:', value: mapped, colorCls: 'text-purple-600' }, ]) ); })()}
{/* Ideas */} -
+
-
- +
+
-
Ideas
+
Ideas
{(() => { const res = getStageResult(3); const total = res?.total ?? pipelineOverview[2]?.counts?.total ?? metrics?.ideas?.total ?? pipelineOverview[2]?.pending ?? 0; return (
-
{total}
+
{total}
); })()} @@ -643,29 +643,29 @@ const AutomationPage: React.FC = () => { const completed = res?.completed ?? pipelineOverview[2]?.counts?.completed ?? metrics?.ideas?.completed ?? 0; return ( renderMetricRow([ - { label: 'New:', value: newCount, colorCls: 'text-warning-700' }, - { label: 'Queued:', value: queued, colorCls: 'text-warning-700' }, - { label: 'Completed:', value: completed, colorCls: 'text-warning-700' }, + { label: 'New:', value: newCount, colorCls: 'text-warning-600' }, + { label: 'Queued:', value: queued, colorCls: 'text-warning-600' }, + { label: 'Completed:', value: completed, colorCls: 'text-warning-600' }, ]) ); })()}
{/* Content */} -
+
-
- +
+
-
Content
+
Content
{(() => { const res = getStageResult(4); const total = res?.total ?? pipelineOverview[3]?.counts?.total ?? metrics?.content?.total ?? pipelineOverview[3]?.pending ?? 0; return (
-
{total}
+
{total}
); })()} @@ -677,29 +677,29 @@ const AutomationPage: React.FC = () => { const publish = res?.published ?? res?.publish ?? pipelineOverview[3]?.counts?.published ?? metrics?.content?.published ?? 0; return ( renderMetricRow([ - { label: 'Draft:', value: draft, colorCls: 'text-success-700' }, - { label: 'Review:', value: review, colorCls: 'text-success-700' }, - { label: 'Publish:', value: publish, colorCls: 'text-success-700' }, + { label: 'Draft:', value: draft, colorCls: 'text-success-600' }, + { label: 'Review:', value: review, colorCls: 'text-success-600' }, + { label: 'Publish:', value: publish, colorCls: 'text-success-600' }, ]) ); })()}
{/* Images */} -
+
-
- +
+
-
Images
+
Images
{(() => { const res = getStageResult(6); const total = res?.total ?? pipelineOverview[5]?.counts?.total ?? metrics?.images?.total ?? pipelineOverview[5]?.pending ?? 0; return (
-
{total}
+
{total}
); })()} @@ -708,17 +708,17 @@ const AutomationPage: React.FC = () => { 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-purple-700' })); + 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-purple-700' })); + 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-purple-700' }, + { label: 'Pending:', value: pipelineOverview[5]?.pending ?? metrics?.images?.pending ?? 0, colorCls: 'text-info-600' }, ]); })()}
@@ -794,19 +794,25 @@ const AutomationPage: React.FC = () => { : dbPending; const progressPercent = total > 0 ? Math.min(Math.round((processed / total) * 100), 100) : 0; + + // Determine the left border color based on stage + const stageBorderColors = ['border-l-brand-500', 'border-l-purple-500', 'border-l-warning-500', 'border-l-gray-600']; + const stageBorderColor = stageBorderColors[index] || 'border-l-brand-500'; return (
0 - ? `border-gray-200 bg-white dark:bg-white/[0.03] dark:border-gray-800 ${stageConfig.hoverColor} hover:shadow-lg` - : 'border-gray-200 bg-gray-50 dark:bg-white/[0.02] dark:border-gray-800' + ? `${stageConfig.hoverColor} hover:shadow-lg` + : '' + } } `} > @@ -918,19 +924,24 @@ const AutomationPage: React.FC = () => { : dbPending; const progressPercent = total > 0 ? Math.min(Math.round((processed / total) * 100), 100) : 0; + + // Determine the left border color based on stage (5=brand, 6=purple) + const stageBorderColors56 = ['border-l-brand-500', 'border-l-purple-500']; + const stageBorderColor = stageBorderColors56[index] || 'border-l-brand-500'; return (
0 - ? `border-gray-200 bg-white dark:bg-white/[0.03] dark:border-gray-800 ${stageConfig.hoverColor} hover:shadow-lg` - : 'border-gray-200 bg-gray-50 dark:bg-white/[0.02] dark:border-gray-800' + ? `${stageConfig.hoverColor} hover:shadow-lg` + : '' } `} > @@ -1030,14 +1041,15 @@ const AutomationPage: React.FC = () => { return (
0 - ? 'border-success-300 bg-success-50 dark:bg-success-900/20 dark:border-success-700' - : 'border-gray-200 bg-gray-50 dark:bg-white/[0.02] dark:border-gray-800' + ? 'hover:border-success-500 hover:shadow-lg' + : '' } `} > diff --git a/frontend/src/pages/Publisher/ContentCalendar.tsx b/frontend/src/pages/Publisher/ContentCalendar.tsx index f417d512..cf54c59e 100644 --- a/frontend/src/pages/Publisher/ContentCalendar.tsx +++ b/frontend/src/pages/Publisher/ContentCalendar.tsx @@ -35,16 +35,23 @@ import { type ViewMode = 'list' | 'calendar'; -// API function to schedule content -async function scheduleContent(contentId: number, scheduledDate: string): Promise { +// Type for schedule API response (partial content data) +interface ScheduleResponse { + content_id: number; + site_status: string; + scheduled_publish_at: string; +} + +// API function to schedule content - returns partial data, not full Content +async function scheduleContent(contentId: number, scheduledDate: string): Promise { return fetchAPI(`/v1/writer/content/${contentId}/schedule/`, { method: 'POST', body: JSON.stringify({ scheduled_publish_at: scheduledDate }), }); } -// API function to unschedule content -async function unscheduleContent(contentId: number): Promise { +// API function to unschedule content - returns partial data +async function unscheduleContent(contentId: number): Promise<{ content_id: number; site_status: string }> { return fetchAPI(`/v1/writer/content/${contentId}/unschedule/`, { method: 'POST', }); @@ -59,11 +66,15 @@ export default function ContentCalendar() { const [allContent, setAllContent] = useState([]); const [viewMode, setViewMode] = useState('calendar'); // Default to calendar view const [draggedItem, setDraggedItem] = useState(null); + const [currentMonth, setCurrentMonth] = useState(new Date()); // Track current month for calendar - // Derived state: Queue items (scheduled or publishing - future dates only for queue) + // Derived state: Queue items (scheduled or publishing - exclude already published) const queueItems = useMemo(() => { return allContent - .filter((c: Content) => c.site_status === 'scheduled' || c.site_status === 'publishing') + .filter((c: Content) => + (c.site_status === 'scheduled' || c.site_status === 'publishing') && + (!c.external_id || c.external_id === '') // Exclude already published 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; @@ -71,19 +82,19 @@ export default function ContentCalendar() { }); }, [allContent]); - // Derived state: Published items (site_status = 'published') + // Derived state: Published items (have external_id - same logic as Content Approved page) const publishedItems = useMemo(() => { - return allContent.filter((c: Content) => c.site_status === 'published'); + return allContent.filter((c: Content) => c.external_id && c.external_id !== ''); }, [allContent]); - // Derived state: Approved items for sidebar (approved but not scheduled/publishing/published) + // Derived state: Approved items for sidebar (approved but not published to site) const approvedItems = useMemo(() => { return allContent.filter( (c: Content) => c.status === 'approved' && + (!c.external_id || c.external_id === '') && // Not published to site c.site_status !== 'scheduled' && - c.site_status !== 'publishing' && - c.site_status !== 'published' + c.site_status !== 'publishing' ); }, [allContent]); @@ -93,26 +104,31 @@ export default function ContentCalendar() { const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); const thirtyDaysFromNow = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000); - // Published in last 30 days + // Published in last 30 days (items with external_id) const publishedLast30Days = allContent.filter((c: Content) => { - if (c.site_status !== 'published') return false; - const publishDate = c.site_status_updated_at ? new Date(c.site_status_updated_at) : null; + if (!c.external_id || c.external_id === '') return false; + // Use updated_at as publish date since site_status_updated_at may not be set + const publishDate = c.updated_at ? new Date(c.updated_at) : null; return publishDate && publishDate >= thirtyDaysAgo; }).length; - // Scheduled in next 30 days + // Scheduled in next 30 days (exclude already published items with external_id) const scheduledNext30Days = allContent.filter((c: Content) => { if (c.site_status !== 'scheduled') return false; + if (c.external_id && c.external_id !== '') return false; // Exclude already published const schedDate = c.scheduled_publish_at ? new Date(c.scheduled_publish_at) : null; return schedDate && schedDate >= now && schedDate <= thirtyDaysFromNow; }).length; return { - scheduled: allContent.filter((c: Content) => c.site_status === 'scheduled').length, + // Scheduled count excludes items that are already published (have external_id) + scheduled: allContent.filter((c: Content) => + c.site_status === 'scheduled' && (!c.external_id || c.external_id === '') + ).length, publishing: allContent.filter((c: Content) => c.site_status === 'publishing').length, - published: allContent.filter((c: Content) => c.site_status === 'published').length, + published: allContent.filter((c: Content) => c.external_id && c.external_id !== '').length, review: allContent.filter((c: Content) => c.status === 'review').length, - approved: allContent.filter((c: Content) => c.status === 'approved' && c.site_status !== 'published').length, + approved: allContent.filter((c: Content) => c.status === 'approved' && (!c.external_id || c.external_id === '')).length, publishedLast30Days, scheduledNext30Days, }; @@ -130,6 +146,20 @@ export default function ContentCalendar() { page_size: 200, }); + // Debug: Log content with external_id (published status) + console.log('[ContentCalendar] Total content items:', response.results?.length); + console.log('[ContentCalendar] Published items (with external_id):', response.results?.filter(c => c.external_id && c.external_id !== '').length); + console.log('[ContentCalendar] Scheduled items:', response.results?.filter(c => c.site_status === 'scheduled').length); + console.log('[ContentCalendar] Sample content:', response.results?.slice(0, 3).map(c => ({ + id: c.id, + title: c.title, + status: c.status, + site_status: c.site_status, + external_id: c.external_id, + scheduled_publish_at: c.scheduled_publish_at, + updated_at: c.updated_at + }))); + setAllContent(response.results || []); } catch (error: any) { toast.error(`Failed to load content: ${error.message}`); @@ -142,7 +172,7 @@ export default function ContentCalendar() { if (activeSite?.id) { loadQueue(); } - }, [activeSite?.id, loadQueue]); + }, [activeSite?.id]); // Removed loadQueue from dependencies to prevent reload loops // Drag and drop handlers for list view const handleDragStart = (e: React.DragEvent, item: Content, source: 'queue' | 'approved') => { @@ -170,9 +200,17 @@ export default function ContentCalendar() { tomorrow.setDate(tomorrow.getDate() + 1); tomorrow.setHours(9, 0, 0, 0); - await scheduleContent(draggedItem.id, tomorrow.toISOString()); + const response = await scheduleContent(draggedItem.id, tomorrow.toISOString()); toast.success(`Scheduled for ${tomorrow.toLocaleDateString()}`); - loadQueue(); // Reload to get updated data + + // Merge API response with existing content - API returns partial data + setAllContent(prevContent => [ + ...prevContent.map(c => c.id === draggedItem.id ? { + ...c, + site_status: response.site_status, + scheduled_publish_at: response.scheduled_publish_at, + } : c) + ]); } catch (error: any) { toast.error(`Failed to schedule: ${error.message}`); } @@ -191,9 +229,17 @@ export default function ContentCalendar() { newDate.setHours(9, 0, 0, 0); try { - await scheduleContent(draggedItem.id, newDate.toISOString()); + const response = await scheduleContent(draggedItem.id, newDate.toISOString()); toast.success(`Scheduled for ${newDate.toLocaleDateString()}`); - loadQueue(); // Reload to get updated data from server + + // Merge API response with existing content - API returns partial data + setAllContent(prevContent => [ + ...prevContent.map(c => c.id === draggedItem.id ? { + ...c, + site_status: response.site_status, + scheduled_publish_at: response.scheduled_publish_at, + } : c) + ]); } catch (error: any) { toast.error(`Failed to schedule: ${error.message}`); } @@ -209,7 +255,15 @@ export default function ContentCalendar() { try { await unscheduleContent(item.id); toast.success('Removed from queue'); - loadQueue(); // Reload to get updated data + + // Update state directly - set site_status to not_published and clear scheduled_publish_at + setAllContent(prevContent => [ + ...prevContent.map(c => c.id === item.id ? { + ...c, + site_status: 'not_published', + scheduled_publish_at: null, + } : c) + ]); } catch (error: any) { toast.error(`Failed to remove: ${error.message}`); } @@ -251,24 +305,56 @@ export default function ContentCalendar() { // Calendar view helpers const getCalendarDays = () => { - const today = new Date(); const days = []; - // Start from beginning of current week - const startOfWeek = new Date(today); - startOfWeek.setDate(today.getDate() - today.getDay()); + // Get first day of the month + const firstDayOfMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), 1); + // Get last day of the month + const lastDayOfMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 0); - // Show 4 weeks - for (let i = 0; i < 28; i++) { - const date = new Date(startOfWeek); - date.setDate(startOfWeek.getDate() + i); + // Start from the Sunday before or on the first day of month + const startDate = new Date(firstDayOfMonth); + startDate.setDate(startDate.getDate() - startDate.getDay()); + + // Calculate how many days to show (must be multiple of 7) + const daysInMonth = lastDayOfMonth.getDate(); + const startDayOffset = firstDayOfMonth.getDay(); + const totalCells = Math.ceil((daysInMonth + startDayOffset) / 7) * 7; + + // Generate all days for the calendar grid + for (let i = 0; i < totalCells; i++) { + const date = new Date(startDate); + date.setDate(startDate.getDate() + i); days.push(date); } return days; }; - // Get scheduled items for a specific date + // Navigation functions + const goToPreviousMonth = () => { + setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1)); + }; + + const goToNextMonth = () => { + setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1)); + }; + + const goToToday = () => { + setCurrentMonth(new Date()); + }; + + // Truncate title to max words + const truncateTitle = (title: string | undefined, maxWords: number = 7) => { + if (!title) return ''; + const words = title.split(' '); + if (words.length <= maxWords) return title; + return words.slice(0, maxWords).join(' ') + '...'; + }; + + // Get scheduled items for a specific date (exclude already published) const getScheduledItemsForDate = (date: Date) => { return queueItems.filter(item => { + // Skip if already published (has external_id) + if (item.external_id && item.external_id !== '') return false; if (!item.scheduled_publish_at) return false; const itemDate = new Date(item.scheduled_publish_at); return ( @@ -282,8 +368,8 @@ export default function ContentCalendar() { // Get published items for a specific date const getPublishedItemsForDate = (date: Date) => { return publishedItems.filter(item => { - // Use site_status_updated_at as publish date - const publishDate = item.site_status_updated_at || item.updated_at; + // Use updated_at as publish date (when external_id was set) + const publishDate = item.updated_at; if (!publishDate) return false; const itemDate = new Date(publishDate); return ( @@ -321,14 +407,11 @@ export default function ContentCalendar() {
- {/* Header - Site selector is in app header */} -
- , color: 'amber' }} - hideSiteSector - /> -
+ , color: 'amber' }} + hideSiteSector + /> {/* Stats Overview - New layout with count on right, bigger labels, descriptions */}
@@ -464,9 +547,11 @@ export default function ContentCalendar() { tomorrow.setDate(tomorrow.getDate() + 1); tomorrow.setHours(9, 0, 0, 0); scheduleContent(draggedItem.id, tomorrow.toISOString()) - .then(() => { + .then((updatedContent) => { toast.success(`Scheduled for ${tomorrow.toLocaleDateString()}`); - loadQueue(); + setAllContent(prevContent => [ + ...prevContent.map(c => c.id === draggedItem.id ? updatedContent : c) + ]); }) .catch((err) => toast.error(`Failed to schedule: ${err.message}`)); } @@ -532,7 +617,40 @@ export default function ContentCalendar() { ) : ( /* Calendar View with drag-drop */ - + + + ‹} + variant="ghost" + tone="neutral" + size="sm" + onClick={goToPreviousMonth} + title="Previous month" + /> +

+ {currentMonth.toLocaleString('default', { month: 'long', year: 'numeric' })} +

+ ›} + variant="ghost" + tone="neutral" + size="sm" + onClick={goToNextMonth} + title="Next month" + /> +
+ } + >
{/* Day headers */} {['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(day => ( @@ -547,6 +665,9 @@ export default function ContentCalendar() { const publishedOnDate = getPublishedItemsForDate(date); const isToday = date.toDateString() === new Date().toDateString(); const isPast = date < new Date(new Date().setHours(0, 0, 0, 0)); + const isCurrentMonth = date.getMonth() === currentMonth.getMonth(); + const totalItems = scheduledItems.length + publishedOnDate.length; + const hasMoreThan5 = totalItems > 5; return (
handleDropOnCalendarDate(e, date) : undefined} className={` - min-h-[100px] p-2 rounded-lg border transition-colors + p-2 rounded-lg border transition-colors ${isToday ? 'border-brand-500 bg-brand-50 dark:bg-brand-900/20' : isPast ? 'border-gray-100 dark:border-gray-800 bg-gray-50/50 dark:bg-gray-900/30' : 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800' } + ${!isCurrentMonth ? 'opacity-40' : ''} ${!isPast && draggedItem ? 'border-dashed border-brand-300 dark:border-brand-600' : ''} `} > @@ -574,26 +696,26 @@ export default function ContentCalendar() {
{/* Published items with glass effect */} - {publishedOnDate.slice(0, 2).map(item => ( + {publishedOnDate.slice(0, 5).map(item => (
handleViewContent(item)} - className="text-xs p-1.5 bg-success-100/60 dark:bg-success-900/20 text-success-700 dark:text-success-300 rounded truncate cursor-pointer backdrop-blur-sm border border-success-200/50 dark:border-success-800/50" + className="text-xs p-1.5 bg-success-100/60 dark:bg-success-900/20 text-success-700 dark:text-success-300 rounded cursor-pointer backdrop-blur-sm border border-success-200/50 dark:border-success-800/50 break-words" > - ✓ {item.title} + ✓ {truncateTitle(item.title, 7)}
))} {/* Scheduled items */} - {scheduledItems.slice(0, 3 - publishedOnDate.length).map(item => ( + {scheduledItems.slice(0, Math.max(0, 5 - publishedOnDate.length)).map(item => ( handleDragStart(e, item, 'queue') : undefined} onDragEnd={handleDragEnd} onClick={() => handleViewContent(item)} - className={`text-xs p-1.5 rounded truncate transition-colors ${ + className={`text-xs p-1.5 rounded transition-colors break-words ${ isPast ? 'bg-gray-100/60 dark:bg-gray-800/40 text-gray-500 dark:text-gray-400 backdrop-blur-sm cursor-default' : 'bg-warning-100 dark:bg-warning-900/30 text-warning-800 dark:text-warning-200 cursor-move hover:bg-warning-200 dark:hover:bg-warning-900/50' }`} > - {item.title} + {truncateTitle(item.title, 7)}
))} - {(scheduledItems.length + publishedOnDate.length) > 3 && ( -
- +{(scheduledItems.length + publishedOnDate.length) - 3} more -
+ {hasMoreThan5 && ( + )}
diff --git a/frontend/src/pages/Sites/Content.tsx b/frontend/src/pages/Sites/Content.tsx index 88c10b7d..c9a8c1ba 100644 --- a/frontend/src/pages/Sites/Content.tsx +++ b/frontend/src/pages/Sites/Content.tsx @@ -12,7 +12,8 @@ import Button from '../../components/ui/button/Button'; import InputField from '../../components/form/input/InputField'; import Select from '../../components/form/Select'; import { useToast } from '../../components/ui/toast/ToastContainer'; -import { fetchAPI } from '../../services/api'; +import { fetchAPI, setActiveSite as apiSetActiveSite } from '../../services/api'; +import { useSiteStore } from '../../store/siteStore'; import { SearchIcon } from '../../icons'; import { PencilIcon, @@ -20,8 +21,10 @@ import { TrashBinIcon, PlusIcon, FileIcon, - GridIcon + GridIcon, + GlobeIcon } from '../../icons'; +import SiteInfoBar from '../../components/common/SiteInfoBar'; interface ContentItem { id: number; @@ -40,6 +43,7 @@ export default function SiteContentManager() { const { id: siteId } = useParams<{ id: string }>(); const navigate = useNavigate(); const toast = useToast(); + const { setActiveSite } = useSiteStore(); const [content, setContent] = useState([]); const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(''); @@ -50,13 +54,36 @@ export default function SiteContentManager() { const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(1); const [totalCount, setTotalCount] = useState(0); + const [site, setSite] = useState(null); const pageSize = 20; + useEffect(() => { + if (siteId) { + loadSiteAndContent(); + } + }, [siteId]); + useEffect(() => { if (siteId) { loadContent(); } - }, [siteId, currentPage, statusFilter, sourceFilter, searchTerm, sortBy, sortDirection]); + }, [currentPage, statusFilter, sourceFilter, searchTerm, sortBy, sortDirection]); + + const loadSiteAndContent = async () => { + try { + // Load site data and sync with store + const siteData = await fetchAPI(`/v1/auth/sites/${siteId}/`); + if (siteData) { + setSite(siteData); + setActiveSite(siteData); + await apiSetActiveSite(siteData.id).catch(() => {}); + } + // Then load content + await loadContent(); + } catch (error: any) { + console.error('Failed to load site:', error); + } + }; const loadContent = async () => { try { @@ -127,15 +154,13 @@ export default function SiteContentManager() { , color: 'blue' }} + title="Content Manager" + badge={{ icon: , color: 'green' }} hideSiteSector /> -
- -
+ + {/* Site Info Bar */} + {/* Filters */} diff --git a/frontend/src/pages/Sites/Dashboard.tsx b/frontend/src/pages/Sites/Dashboard.tsx index b18955c5..30184bec 100644 --- a/frontend/src/pages/Sites/Dashboard.tsx +++ b/frontend/src/pages/Sites/Dashboard.tsx @@ -8,16 +8,19 @@ import { useParams, useNavigate } from 'react-router-dom'; import PageMeta from '../../components/common/PageMeta'; import PageHeader from '../../components/common/PageHeader'; import ComponentCard from '../../components/common/ComponentCard'; +import SiteInfoBar from '../../components/common/SiteInfoBar'; import { Card } from '../../components/ui/card'; import Button from '../../components/ui/button/Button'; import { useToast } from '../../components/ui/toast/ToastContainer'; -import { fetchAPI, fetchSiteSectors } from '../../services/api'; +import { fetchAPI, fetchSiteSectors, setActiveSite as apiSetActiveSite } from '../../services/api'; +import { getDashboardStats } from '../../services/billing.api'; import SiteSetupChecklist from '../../components/sites/SiteSetupChecklist'; import { integrationApi } from '../../services/integration.api'; import SiteConfigWidget from '../../components/dashboard/SiteConfigWidget'; import OperationsCostsWidget from '../../components/dashboard/OperationsCostsWidget'; import CreditAvailabilityWidget from '../../components/dashboard/CreditAvailabilityWidget'; import { useBillingStore } from '../../store/billingStore'; +import { useSiteStore } from '../../store/siteStore'; import { FileIcon, PlugInIcon, @@ -27,6 +30,7 @@ import { ArrowRightIcon, ArrowUpIcon, ClockIcon, + ChevronRightIcon, } from '../../icons'; interface Site { @@ -67,6 +71,7 @@ export default function SiteDashboard() { const navigate = useNavigate(); const toast = useToast(); const { balance, loadBalance } = useBillingStore(); + const { setActiveSite } = useSiteStore(); const [site, setSite] = useState(null); const [setupState, setSetupState] = useState({ hasIndustry: false, @@ -83,19 +88,33 @@ export default function SiteDashboard() { useEffect(() => { if (siteId) { - loadSiteData(); - loadBalance(); - } - }, [siteId, loadBalance]); - - const loadSiteData = async () => { - try { + // Create a local copy of siteId to use in async operations + const currentSiteId = siteId; + + // Reset state when site changes + setOperations([]); + setSite(null); setLoading(true); + // Load data for this specific siteId + loadSiteData(currentSiteId); + loadBalance(); + } + }, [siteId]); + + const loadSiteData = async (currentSiteId: string) => { + try { // Load site data - const siteData = await fetchAPI(`/v1/auth/sites/${siteId}/`); + const siteData = await fetchAPI(`/v1/auth/sites/${currentSiteId}/`); + + // CRITICAL: Verify we're still on the same site before updating state + // This prevents race conditions when user rapidly switches sites if (siteData) { setSite(siteData); + // Update global site store so site selector shows correct site + setActiveSite(siteData); + // Also set as active site in backend + await apiSetActiveSite(siteData.id).catch(() => {}); // Check setup state const hasIndustry = !!siteData.industry || !!siteData.industry_name; @@ -104,7 +123,7 @@ export default function SiteDashboard() { let hasSectors = false; let sectorsCount = 0; try { - const sectors = await fetchSiteSectors(Number(siteId)); + const sectors = await fetchSiteSectors(Number(currentSiteId)); hasSectors = sectors && sectors.length > 0; sectorsCount = sectors?.length || 0; } catch (err) { @@ -114,7 +133,7 @@ export default function SiteDashboard() { // Check WordPress integration let hasWordPressIntegration = false; try { - const wpIntegration = await integrationApi.getWordPressIntegration(Number(siteId)); + const wpIntegration = await integrationApi.getWordPressIntegration(Number(currentSiteId)); hasWordPressIntegration = !!wpIntegration; } catch (err) { // No integration is fine @@ -125,7 +144,7 @@ export default function SiteDashboard() { let keywordsCount = 0; try { const { fetchKeywords } = await import('../../services/api'); - const keywordsData = await fetchKeywords({ site_id: Number(siteId), page_size: 1 }); + const keywordsData = await fetchKeywords({ site_id: Number(currentSiteId), page_size: 1 }); hasKeywords = keywordsData?.results?.length > 0 || keywordsData?.count > 0; keywordsCount = keywordsData?.count || 0; } catch (err) { @@ -136,7 +155,7 @@ export default function SiteDashboard() { let hasAuthorProfiles = false; let authorProfilesCount = 0; try { - const authorsData = await fetchAPI(`/v1/thinker/author-profiles/?site_id=${siteId}&page_size=1`); + const authorsData = await fetchAPI(`/v1/thinker/author-profiles/?site_id=${currentSiteId}&page_size=1`); hasAuthorProfiles = authorsData?.count > 0; authorProfilesCount = authorsData?.count || 0; } catch (err) { @@ -154,15 +173,54 @@ export default function SiteDashboard() { authorProfilesCount, }); - // Load operation stats (mock data for now - would come from backend) - // In real implementation, fetch from /api/v1/dashboard/site/{siteId}/operations/ - const mockOperations: OperationStat[] = [ - { type: 'clustering', count: 8, creditsUsed: 80, avgCreditsPerOp: 10 }, - { type: 'ideas', count: 12, creditsUsed: 24, avgCreditsPerOp: 2 }, - { type: 'content', count: 28, creditsUsed: 1400, avgCreditsPerOp: 50 }, - { type: 'images', count: 45, creditsUsed: 225, avgCreditsPerOp: 5 }, - ]; - setOperations(mockOperations); + // Load operation stats from real API data + try { + const stats = await getDashboardStats({ site_id: Number(currentSiteId), days: 7 }); + + // Map operation types from API to display types + const operationTypeMap: Record = { + 'clustering': 'clustering', + 'idea_generation': 'ideas', + 'content_generation': 'content', + 'image_generation': 'images', + 'image_prompt_extraction': 'images', + }; + + const mappedOperations: OperationStat[] = []; + const expectedTypes: Array<'clustering' | 'ideas' | 'content' | 'images'> = ['clustering', 'ideas', 'content', 'images']; + + // Initialize with zeros + const opTotals: Record = {}; + expectedTypes.forEach(t => { opTotals[t] = { count: 0, credits: 0 }; }); + + // Sum up operations by mapped type + if (stats.ai_operations?.operations) { + stats.ai_operations.operations.forEach(op => { + const mappedType = operationTypeMap[op.type] || op.type; + if (opTotals[mappedType]) { + opTotals[mappedType].count += op.count; + opTotals[mappedType].credits += op.credits; + } + }); + } + + // Convert to array with avgCreditsPerOp + expectedTypes.forEach(type => { + const data = opTotals[type]; + mappedOperations.push({ + type, + count: data.count, + creditsUsed: data.credits, + avgCreditsPerOp: data.count > 0 ? data.credits / data.count : 0, + }); + }); + + setOperations(mappedOperations); + } catch (err) { + console.log('Could not load operations stats:', err); + // Set empty operations if API fails + setOperations([]); + } } } catch (error: any) { toast.error(`Failed to load site data: ${error.message}`); @@ -200,33 +258,17 @@ export default function SiteDashboard() {
, color: 'blue' }} - breadcrumb="Sites / Dashboard" + hideSiteSector /> - {/* Site Info */} -
-

- {site.slug} • {site.site_type} • {site.hosting_type} -

- {site.domain && ( -

- {site.domain} -

- )} -
- -
-
+ {/* Site Info Bar */} + - {/* Site Setup Checklist */} -
+ {/* Site Setup Progress + Quick Actions - Side by Side */} +
+ {/* Site Setup Checklist - Left Half */} + + {/* Quick Actions - Right Half */} + +
+ {/* Manage Pages */} + + + {/* Manage Content */} + + + {/* Integrations */} + + + {/* Sync Dashboard */} + + + {/* Deploy Site */} + + + {/* Content Calendar */} + +
+
{/* Site Insights - 3 Column Grid */}
- +
- {/* Quick Actions */} - -
- - - - - - - - - - - -
-
- {/* Recent Activity - Placeholder */} - +

Recent Activity

diff --git a/frontend/src/pages/Sites/Settings.tsx b/frontend/src/pages/Sites/Settings.tsx index 3fddec2b..7d408677 100644 --- a/frontend/src/pages/Sites/Settings.tsx +++ b/frontend/src/pages/Sites/Settings.tsx @@ -24,19 +24,23 @@ import { fetchIndustries, Site, Industry, + setActiveSite as apiSetActiveSite, } from '../../services/api'; +import { useSiteStore } from '../../store/siteStore'; import WordPressIntegrationForm from '../../components/sites/WordPressIntegrationForm'; import { integrationApi, SiteIntegration } from '../../services/integration.api'; -import { GridIcon, PlugInIcon, PaperPlaneIcon, DocsIcon, BoltIcon, FileIcon, ChevronDownIcon, CloseIcon, PlusIcon, RefreshCwIcon, FileTextIcon, ImageIcon, SaveIcon, Loader2Icon } from '../../icons'; +import { GridIcon, PlugInIcon, PaperPlaneIcon, DocsIcon, BoltIcon, FileIcon, ChevronDownIcon, CloseIcon, PlusIcon, RefreshCwIcon, FileTextIcon, ImageIcon, SaveIcon, Loader2Icon, ArrowRightIcon, SettingsIcon, GlobeIcon, LayersIcon } from '../../icons'; import Badge from '../../components/ui/badge/Badge'; import { Dropdown } from '../../components/ui/dropdown/Dropdown'; import { DropdownItem } from '../../components/ui/dropdown/DropdownItem'; +import SiteInfoBar from '../../components/common/SiteInfoBar'; export default function SiteSettings() { const { id: siteId } = useParams<{ id: string }>(); const navigate = useNavigate(); const [searchParams] = useSearchParams(); const toast = useToast(); + const { setActiveSite } = useSiteStore(); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [site, setSite] = useState(null); @@ -55,6 +59,9 @@ export default function SiteSettings() { const [contentTypes, setContentTypes] = useState(null); const [contentTypesLoading, setContentTypesLoading] = useState(false); + // Advanced Settings toggle + const [showAdvancedSettings, setShowAdvancedSettings] = useState(false); + // Publishing settings state const [publishingSettings, setPublishingSettings] = useState(null); const [publishingSettingsLoading, setPublishingSettingsLoading] = useState(false); @@ -281,6 +288,10 @@ export default function SiteSettings() { const data = await fetchAPI(`/v1/auth/sites/${siteId}/`); if (data) { setSite(data); + // Update global site store so site selector shows correct site + setActiveSite(data); + // Also set as active site in backend + await apiSetActiveSite(data.id).catch(() => {}); const seoData = data.seo_metadata || data.metadata || {}; setFormData({ name: data.name || '', @@ -746,116 +757,46 @@ export default function SiteSettings() { return (
+ , color: 'blue' }} + hideSiteSector + /> -
-
- , color: 'blue' }} - hideSiteSector - /> - {/* Integration status indicator */} -
- - - {integrationStatus === 'connected' && 'Connected'} - {integrationStatus === 'configured' && (testingAuth ? 'Testing...' : 'Configured')} - {integrationStatus === 'not_configured' && 'Not configured'} - -
-
- - {/* Site Selector - Only show if more than 1 site */} - {!sitesLoading && sites.length > 1 && ( -
- - setIsSiteSelectorOpen(false)} - anchorRef={siteSelectorRef} - > - {sites.map((s) => ( - handleSiteSelect(s.id)} - className={`flex items-center gap-3 px-3 py-2 font-medium rounded-lg text-sm text-left ${ - site?.id === s.id - ? "bg-brand-50 text-brand-700 dark:bg-brand-500/20 dark:text-brand-300" - : "text-gray-700 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300" - }`} - > - {s.name} - {site?.id === s.id && ( - - - - )} - - ))} - -
- )} -
+ {/* Site Info Bar */} + {/* Tabs */}
-
- - + @@ -867,10 +808,10 @@ export default function SiteSettings() { }} className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors whitespace-nowrap ${ activeTab === 'image-settings' - ? 'border-brand-500 text-brand-600 dark:text-brand-400' + ? 'border-purple-500 text-purple-600 dark:text-purple-400' : 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300' }`} - startIcon={} + startIcon={} > Images @@ -882,10 +823,10 @@ export default function SiteSettings() { }} className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors whitespace-nowrap ${ activeTab === 'integrations' - ? 'border-brand-500 text-brand-600 dark:text-brand-400' + ? 'border-warning-500 text-warning-600 dark:text-warning-400' : 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300' }`} - startIcon={} + startIcon={} > Integrations @@ -897,10 +838,10 @@ export default function SiteSettings() { }} className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors whitespace-nowrap ${ activeTab === 'publishing' - ? 'border-brand-500 text-brand-600 dark:text-brand-400' + ? '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={} + startIcon={} > Publishing @@ -913,21 +854,37 @@ export default function SiteSettings() { }} className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors whitespace-nowrap ${ activeTab === 'content-types' - ? 'border-brand-500 text-brand-600 dark:text-brand-400' + ? 'border-error-500 text-error-600 dark:text-error-400' : 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300' }`} - startIcon={} + startIcon={} > Content Types )} +
+ + {/* Integration Status Indicator - Larger */} +
+ + + {integrationStatus === 'connected' && 'Connected'} + {integrationStatus === 'configured' && (testingAuth ? 'Testing...' : 'Configured')} + {integrationStatus === 'not_configured' && 'Not Configured'} + +
{/* Content Generation Tab */} {activeTab === 'content-generation' && (
- +
@@ -1010,7 +967,7 @@ export default function SiteSettings() { {/* Image Settings Tab */} {activeTab === 'image-settings' && (
- +
@@ -1533,10 +1490,10 @@ export default function SiteSettings() { {/* General Tab */} {activeTab === 'general' && ( <> - {/* 4-Card Layout for Basic Settings, SEO, Open Graph, and Schema */} + {/* Row 1: Basic Settings and Industry/Sectors side by side */}
{/* Card 1: Basic Site Settings */} - +

Basic Settings @@ -1598,289 +1555,316 @@ export default function SiteSettings() {

- {/* Card 2: SEO Meta Tags */} - + {/* Card 2: Industry & Sectors Configuration */} +

- - SEO Meta Tags + + Industry & Sectors

+

+ Configure up to 5 sectors for content targeting. +

+
- - setFormData({ ...formData, meta_title: e.target.value })} - placeholder="SEO title (recommended: 50-60 characters)" - max="60" + +