ui frotneedn fixes
This commit is contained in:
@@ -386,12 +386,13 @@ export default function ImageGenerationCard({
|
||||
<Button
|
||||
onClick={handleGenerate}
|
||||
disabled={isGenerating || !prompt.trim()}
|
||||
className="inline-flex items-center gap-2 px-6 py-2.5"
|
||||
variant="primary"
|
||||
size="md"
|
||||
>
|
||||
{isGenerating ? (
|
||||
<>
|
||||
<svg
|
||||
className="h-4 w-4 animate-spin"
|
||||
className="h-4 w-4 animate-spin mr-2"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
|
||||
@@ -19,6 +19,7 @@ interface PageHeaderProps {
|
||||
color: 'blue' | 'green' | 'purple' | 'orange' | 'red' | 'indigo';
|
||||
};
|
||||
hideSiteSector?: boolean; // Hide site/sector selector and info for global pages
|
||||
navigation?: ReactNode; // Module navigation tabs
|
||||
}
|
||||
|
||||
export default function PageHeader({
|
||||
@@ -29,6 +30,7 @@ export default function PageHeader({
|
||||
className = "",
|
||||
badge,
|
||||
hideSiteSector = false,
|
||||
navigation,
|
||||
}: PageHeaderProps) {
|
||||
const { activeSite } = useSiteStore();
|
||||
const { activeSector } = useSectorStore();
|
||||
@@ -44,6 +46,7 @@ export default function PageHeader({
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 ${className}`}>
|
||||
{/* Left side: Title, badge, and site/sector info */}
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3">
|
||||
{badge && (
|
||||
@@ -98,16 +101,21 @@ export default function PageHeader({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{!hideSiteSector && <SiteAndSectorSelector />}
|
||||
{showRefresh && onRefresh && (
|
||||
<button
|
||||
onClick={onRefresh}
|
||||
className="px-4 py-2 text-sm font-medium text-brand-500 hover:text-brand-600 border border-brand-200 rounded-lg hover:bg-brand-50 dark:border-brand-800 dark:hover:bg-brand-500/10 transition-colors"
|
||||
>
|
||||
Refresh
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Right side: Navigation bar stacked above site/sector selector */}
|
||||
<div className="flex flex-col items-end gap-3">
|
||||
{navigation && <div>{navigation}</div>}
|
||||
<div className="flex items-center gap-3">
|
||||
{!hideSiteSector && <SiteAndSectorSelector />}
|
||||
{showRefresh && onRefresh && (
|
||||
<button
|
||||
onClick={onRefresh}
|
||||
className="px-4 py-2 text-sm font-medium text-brand-500 hover:text-brand-600 border border-brand-200 rounded-lg hover:bg-brand-50 dark:border-brand-800 dark:hover:bg-brand-500/10 transition-colors"
|
||||
>
|
||||
Refresh
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -228,8 +228,7 @@ export default function SiteIntegrationsSection({ siteId }: SiteIntegrationsSect
|
||||
Connect your sites to external platforms (WordPress, Shopify, Custom APIs)
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={handleAdd} variant="primary">
|
||||
<PlusIcon className="w-4 h-4 mr-2" />
|
||||
<Button onClick={handleAdd} variant="primary" startIcon={<PlusIcon className="w-4 h-4" />}>
|
||||
Add Integration
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import { Link, useLocation } from 'react-router';
|
||||
import { Tabs, TabList, Tab } from '../ui/tabs/Tabs';
|
||||
import Button from '../ui/button/Button';
|
||||
|
||||
export interface NavigationTab {
|
||||
label: string;
|
||||
@@ -32,34 +32,22 @@ export default function ModuleNavigationTabs({ tabs, className = '' }: ModuleNav
|
||||
const activeTabId = activeTab?.path || tabs[0]?.path || '';
|
||||
|
||||
return (
|
||||
<div className={`mb-6 ${className}`}>
|
||||
<Tabs defaultTab={activeTabId}>
|
||||
{(activeTabId, setActiveTab) => (
|
||||
<TabList className="bg-gray-100 dark:bg-gray-900">
|
||||
{tabs.map((tab) => {
|
||||
const isActive = activeTabId === tab.path || location.pathname.startsWith(tab.path + '/');
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={tab.path}
|
||||
to={tab.path}
|
||||
onClick={() => setActiveTab(tab.path)}
|
||||
className="flex-1"
|
||||
>
|
||||
<Tab
|
||||
tabId={tab.path}
|
||||
isActive={isActive}
|
||||
className="flex items-center justify-center gap-2"
|
||||
>
|
||||
{tab.icon && <span className="flex-shrink-0">{tab.icon}</span>}
|
||||
<span>{tab.label}</span>
|
||||
</Tab>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</TabList>
|
||||
)}
|
||||
</Tabs>
|
||||
<div className={`inline-flex gap-2 p-2 rounded-lg bg-gray-50 dark:bg-gray-800/50 shadow-sm border border-gray-200 dark:border-gray-700 ${className}`}>
|
||||
{tabs.map((tab) => {
|
||||
const isActive = activeTabId === tab.path || location.pathname.startsWith(tab.path + '/');
|
||||
|
||||
return (
|
||||
<Link key={tab.path} to={tab.path}>
|
||||
<Button
|
||||
variant={isActive ? 'primary' : 'secondary'}
|
||||
size="sm"
|
||||
startIcon={tab.icon}
|
||||
>
|
||||
{tab.label}
|
||||
</Button>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -411,14 +411,8 @@ export default function WorkflowGuide({ onSiteAdded }: WorkflowGuideProps) {
|
||||
<Button
|
||||
onClick={handleAddSite}
|
||||
disabled={!siteName || !siteName.trim() || !selectedIndustry || selectedSectors.length === 0 || isCreatingSite}
|
||||
variant="solid"
|
||||
tone="brand"
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className={`text-lg font-semibold px-8 py-4 h-auto transition-all duration-200 ${
|
||||
!siteName || !siteName.trim() || !selectedIndustry || selectedSectors.length === 0 || isCreatingSite
|
||||
? ''
|
||||
: 'hover:shadow-lg hover:scale-[1.01] active:scale-[0.99]'
|
||||
}`}
|
||||
>
|
||||
{isCreatingSite ? (
|
||||
<>Creating Site...</>
|
||||
|
||||
@@ -7,13 +7,12 @@
|
||||
import { ReactNode, forwardRef } from "react";
|
||||
import clsx from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
type ButtonSize = "xs" | "sm" | "md" | "lg";
|
||||
type ButtonVariant = "solid" | "soft" | "outline" | "ghost" | "gradient";
|
||||
type ButtonSize = "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
|
||||
type ButtonVariant = "primary" | "secondary" | "outline" | "ghost" | "gradient";
|
||||
type ButtonTone = "brand" | "success" | "warning" | "danger" | "neutral";
|
||||
type ButtonShape = "rounded" | "pill";
|
||||
type ButtonElement = HTMLButtonElement | HTMLAnchorElement;
|
||||
type ButtonElement = HTMLButtonElement;
|
||||
|
||||
interface ButtonProps {
|
||||
children: ReactNode; // Button text or content
|
||||
@@ -28,26 +27,22 @@ interface ButtonProps {
|
||||
fullWidth?: boolean; // Stretch to parent width
|
||||
className?: string; // Additional classes
|
||||
type?: "button" | "submit" | "reset"; // Button type
|
||||
as?: "button" | "a" | typeof Link;
|
||||
href?: string;
|
||||
to?: string; // For React Router Link
|
||||
target?: string;
|
||||
rel?: string;
|
||||
}
|
||||
|
||||
const toneMap: Record<
|
||||
ButtonTone,
|
||||
{
|
||||
solid: string;
|
||||
soft: string;
|
||||
primary: string;
|
||||
secondary: string;
|
||||
outline: string;
|
||||
ghost: string;
|
||||
ring: string;
|
||||
}
|
||||
> = {
|
||||
brand: {
|
||||
solid: "bg-brand-500 text-white hover:bg-brand-600",
|
||||
soft: "bg-brand-50 text-brand-600 hover:bg-brand-100",
|
||||
primary: "bg-brand-500 text-white hover:bg-brand-600",
|
||||
secondary:
|
||||
"bg-[#64748b] text-white hover:bg-[#475569] dark:bg-[#64748b] dark:hover:bg-[#475569]",
|
||||
outline:
|
||||
"text-brand-600 ring-1 ring-brand-200 hover:bg-brand-25 dark:ring-brand-500/40 dark:text-brand-300 dark:hover:bg-brand-500/[0.08]",
|
||||
ghost:
|
||||
@@ -55,8 +50,9 @@ const toneMap: Record<
|
||||
ring: "focus-visible:ring-brand-500",
|
||||
},
|
||||
success: {
|
||||
solid: "bg-success-500 text-white hover:bg-success-600",
|
||||
soft: "bg-success-50 text-success-600 hover:bg-success-100",
|
||||
primary: "bg-success-500 text-white hover:bg-success-600",
|
||||
secondary:
|
||||
"bg-[#64748b] text-white hover:bg-[#475569] dark:bg-[#64748b] dark:hover:bg-[#475569]",
|
||||
outline:
|
||||
"text-success-600 ring-1 ring-success-200 hover:bg-success-25 dark:ring-success-500/40 dark:text-success-300",
|
||||
ghost:
|
||||
@@ -64,8 +60,9 @@ const toneMap: Record<
|
||||
ring: "focus-visible:ring-success-500",
|
||||
},
|
||||
warning: {
|
||||
solid: "bg-warning-500 text-white hover:bg-warning-600",
|
||||
soft: "bg-warning-50 text-warning-600 hover:bg-warning-100",
|
||||
primary: "bg-warning-500 text-white hover:bg-warning-600",
|
||||
secondary:
|
||||
"bg-[#64748b] text-white hover:bg-[#475569] dark:bg-[#64748b] dark:hover:bg-[#475569]",
|
||||
outline:
|
||||
"text-warning-600 ring-1 ring-warning-200 hover:bg-warning-25 dark:ring-warning-500/40 dark:text-warning-300",
|
||||
ghost:
|
||||
@@ -73,8 +70,9 @@ const toneMap: Record<
|
||||
ring: "focus-visible:ring-warning-500",
|
||||
},
|
||||
danger: {
|
||||
solid: "bg-error-500 text-white hover:bg-error-600",
|
||||
soft: "bg-error-50 text-error-600 hover:bg-error-100",
|
||||
primary: "bg-error-500 text-white hover:bg-error-600",
|
||||
secondary:
|
||||
"bg-[#64748b] text-white hover:bg-[#475569] dark:bg-[#64748b] dark:hover:bg-[#475569]",
|
||||
outline:
|
||||
"text-error-600 ring-1 ring-error-200 hover:bg-error-25 dark:ring-error-500/40 dark:text-error-300",
|
||||
ghost:
|
||||
@@ -82,10 +80,10 @@ const toneMap: Record<
|
||||
ring: "focus-visible:ring-error-500",
|
||||
},
|
||||
neutral: {
|
||||
solid:
|
||||
primary:
|
||||
"bg-gray-900 text-white hover:bg-gray-800 dark:bg-white/10 dark:hover:bg-white/20",
|
||||
soft:
|
||||
"bg-gray-100 text-gray-900 hover:bg-gray-200 dark:bg-white/[0.08] dark:text-white dark:hover:bg-white/[0.12]",
|
||||
secondary:
|
||||
"bg-[#64748b] text-white hover:bg-[#475569] dark:bg-[#64748b] dark:hover:bg-[#475569]",
|
||||
outline:
|
||||
"text-gray-700 ring-1 ring-gray-300 hover:bg-gray-50 dark:text-gray-200 dark:ring-white/[0.08] dark:hover:bg-white/[0.04]",
|
||||
ghost:
|
||||
@@ -112,6 +110,8 @@ const sizeClasses: Record<ButtonSize, string> = {
|
||||
sm: "h-9 px-3 text-sm",
|
||||
md: "h-10 px-4 text-sm",
|
||||
lg: "h-12 px-5 text-base",
|
||||
xl: "h-14 px-6 text-lg",
|
||||
"2xl": "h-auto px-8 py-4 text-lg font-semibold",
|
||||
};
|
||||
|
||||
const Button = forwardRef<ButtonElement, ButtonProps>(
|
||||
@@ -119,7 +119,7 @@ const Button = forwardRef<ButtonElement, ButtonProps>(
|
||||
{
|
||||
children,
|
||||
size = "md",
|
||||
variant = "solid",
|
||||
variant = "primary",
|
||||
tone = "brand",
|
||||
shape = "rounded",
|
||||
startIcon,
|
||||
@@ -129,19 +129,14 @@ const Button = forwardRef<ButtonElement, ButtonProps>(
|
||||
disabled = false,
|
||||
fullWidth = false,
|
||||
type = "button",
|
||||
as = "button",
|
||||
href,
|
||||
to,
|
||||
target,
|
||||
rel,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const toneStyles = toneMap[tone];
|
||||
|
||||
const variantClasses: Record<ButtonVariant, string> = {
|
||||
solid: toneStyles.solid,
|
||||
soft: toneStyles.soft,
|
||||
primary: toneStyles.primary,
|
||||
secondary: toneStyles.secondary,
|
||||
outline: clsx(
|
||||
"bg-transparent transition-colors",
|
||||
toneStyles.outline,
|
||||
@@ -173,34 +168,18 @@ const Button = forwardRef<ButtonElement, ButtonProps>(
|
||||
),
|
||||
);
|
||||
|
||||
const Component = as === "a" ? "a" : as === Link ? Link : "button";
|
||||
|
||||
return (
|
||||
<Component
|
||||
<button
|
||||
ref={ref}
|
||||
className={computedClass}
|
||||
onClick={onClick}
|
||||
{...(as === "button"
|
||||
? {
|
||||
type,
|
||||
disabled,
|
||||
}
|
||||
: as === Link
|
||||
? {
|
||||
to,
|
||||
"aria-disabled": disabled,
|
||||
}
|
||||
: {
|
||||
href,
|
||||
target,
|
||||
rel,
|
||||
"aria-disabled": disabled,
|
||||
})}
|
||||
type={type}
|
||||
disabled={disabled}
|
||||
>
|
||||
{startIcon && <span className="flex items-center">{startIcon}</span>}
|
||||
<span className="whitespace-nowrap">{children}</span>
|
||||
{endIcon && <span className="flex items-center">{endIcon}</span>}
|
||||
</Component>
|
||||
</button>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -7,25 +7,6 @@ import NotificationDropdown from "../components/header/NotificationDropdown";
|
||||
import UserDropdown from "../components/header/UserDropdown";
|
||||
import { HeaderMetrics } from "../components/header/HeaderMetrics";
|
||||
import ResourceDebugToggle from "../components/debug/ResourceDebugToggle";
|
||||
import { useOnboardingStore } from "../store/onboardingStore";
|
||||
import Button from "../components/ui/button/Button";
|
||||
import { BoltIcon } from "../icons";
|
||||
|
||||
const ShowGuideButton: React.FC = () => {
|
||||
const { toggleGuide, isGuideVisible } = useOnboardingStore();
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={toggleGuide}
|
||||
className="bg-orange-500 hover:bg-orange-600 text-white border-orange-500 hover:border-orange-600"
|
||||
>
|
||||
<BoltIcon className="w-4 h-4 mr-2" />
|
||||
{isGuideVisible ? 'Hide Guide' : 'Show Guide'}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const AppHeader: React.FC = () => {
|
||||
const [isApplicationMenuOpen, setApplicationMenuOpen] = useState(false);
|
||||
@@ -180,8 +161,6 @@ const AppHeader: React.FC = () => {
|
||||
<div className="flex items-center gap-2 2xsm:gap-3">
|
||||
{/* <!-- Header Metrics (conditional) --> */}
|
||||
<HeaderMetrics />
|
||||
{/* <!-- Show Guide Button (Orange) --> */}
|
||||
<ShowGuideButton />
|
||||
{/* <!-- Dark Mode Toggler --> */}
|
||||
<ThemeToggleButton />
|
||||
{/* <!-- Resource Debug Toggle (Admin only) --> */}
|
||||
|
||||
@@ -194,13 +194,29 @@ const LayoutContent: React.FC = () => {
|
||||
refreshUserData();
|
||||
};
|
||||
|
||||
// Periodic refresh every 2 minutes
|
||||
// Proactive token refresh - refresh token every 12 minutes (before 15-minute expiry)
|
||||
// This prevents 401 errors and ensures seamless user experience
|
||||
const tokenRefreshInterval = setInterval(async () => {
|
||||
const authState = useAuthStore.getState();
|
||||
const refreshToken = authState?.refreshToken;
|
||||
if (refreshToken && authState?.isAuthenticated) {
|
||||
try {
|
||||
await authState.refreshToken();
|
||||
console.debug('Token proactively refreshed');
|
||||
} catch (error) {
|
||||
console.debug('Proactive token refresh failed (will retry on next API call):', error);
|
||||
}
|
||||
}
|
||||
}, 720000); // 12 minutes = 720000ms
|
||||
|
||||
// Periodic user data refresh every 2 minutes
|
||||
const intervalId = setInterval(() => refreshUserData(), 120000);
|
||||
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
window.addEventListener('focus', handleFocus);
|
||||
|
||||
return () => {
|
||||
clearInterval(tokenRefreshInterval);
|
||||
clearInterval(intervalId);
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
window.removeEventListener('focus', handleFocus);
|
||||
|
||||
@@ -127,6 +127,7 @@ export default function AutomationRules() {
|
||||
icon: <BoltIcon />,
|
||||
color: 'purple',
|
||||
}}
|
||||
navigation={<ModuleNavigationTabs tabs={automationTabs} />}
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
@@ -113,8 +113,8 @@ export default function AutomationTasks() {
|
||||
icon: <ClockIcon />,
|
||||
color: 'blue',
|
||||
}}
|
||||
navigation={<ModuleNavigationTabs tabs={automationTabs} />}
|
||||
/>
|
||||
<ModuleNavigationTabs tabs={automationTabs} />
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex-1">
|
||||
|
||||
@@ -104,10 +104,10 @@ export default function LinkerContentList() {
|
||||
<PageHeader
|
||||
title="Link Content"
|
||||
description="Add internal links to your content"
|
||||
navigation={<ModuleNavigationTabs tabs={[
|
||||
{ label: 'Content', path: '/linker/content', icon: <FileIcon /> },
|
||||
]} />}
|
||||
/>
|
||||
<ModuleNavigationTabs tabs={[
|
||||
{ label: 'Content', path: '/linker/content', icon: <FileIcon /> },
|
||||
]} />
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center py-12">
|
||||
|
||||
@@ -148,10 +148,10 @@ export default function OptimizerContentSelector() {
|
||||
icon: <BoltIcon />,
|
||||
color: 'orange',
|
||||
}}
|
||||
navigation={<ModuleNavigationTabs tabs={[
|
||||
{ label: 'Content', path: '/optimizer/content', icon: <FileIcon /> },
|
||||
]} />}
|
||||
/>
|
||||
<ModuleNavigationTabs tabs={[
|
||||
{ label: 'Content', path: '/optimizer/content', icon: <FileIcon /> },
|
||||
]} />
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<select
|
||||
|
||||
@@ -396,8 +396,8 @@ export default function Clusters() {
|
||||
<PageHeader
|
||||
title="Keyword Clusters"
|
||||
badge={{ icon: <GroupIcon />, color: 'purple' }}
|
||||
navigation={<ModuleNavigationTabs tabs={plannerTabs} />}
|
||||
/>
|
||||
<ModuleNavigationTabs tabs={plannerTabs} />
|
||||
<TablePageTemplate
|
||||
columns={pageConfig.columns}
|
||||
data={clusters}
|
||||
|
||||
@@ -306,8 +306,8 @@ export default function Ideas() {
|
||||
<PageHeader
|
||||
title="Content Ideas"
|
||||
badge={{ icon: <BoltIcon />, color: 'orange' }}
|
||||
navigation={<ModuleNavigationTabs tabs={plannerTabs} />}
|
||||
/>
|
||||
<ModuleNavigationTabs tabs={plannerTabs} />
|
||||
<TablePageTemplate
|
||||
columns={pageConfig.columns}
|
||||
data={ideas}
|
||||
|
||||
@@ -764,8 +764,8 @@ export default function Keywords() {
|
||||
<PageHeader
|
||||
title="Keywords"
|
||||
badge={{ icon: <ListIcon />, color: 'green' }}
|
||||
navigation={<ModuleNavigationTabs tabs={plannerTabs} />}
|
||||
/>
|
||||
<ModuleNavigationTabs tabs={plannerTabs} />
|
||||
<TablePageTemplate
|
||||
columns={pageConfig.columns}
|
||||
data={keywords}
|
||||
|
||||
@@ -1062,7 +1062,6 @@ export default function Integration() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Image Generation Testing Cards - 50/50 Split */}
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
<ImageGenerationCard
|
||||
@@ -1141,7 +1140,7 @@ export default function Integration() {
|
||||
<div className="flex justify-between items-center pt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
handleTestConnection();
|
||||
}}
|
||||
@@ -1153,7 +1152,7 @@ export default function Integration() {
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
variant="ghost"
|
||||
onClick={() => setShowSettingsModal(false)}
|
||||
disabled={isSaving || isTesting}
|
||||
>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import PageMeta from '../../components/common/PageMeta';
|
||||
import PageHeader from '../../components/common/PageHeader';
|
||||
import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs';
|
||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||
import {
|
||||
fetchIndustries,
|
||||
@@ -252,6 +253,13 @@ export default function IndustriesSectorsKeywords() {
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
// Navigation tabs for Industries/Sectors/Keywords
|
||||
const setupTabs = [
|
||||
{ label: 'Industries', path: '#industries', icon: <PieChartIcon className="w-4 h-4" /> },
|
||||
{ label: 'Sectors', path: '#sectors', icon: <CheckCircleIcon className="w-4 h-4" /> },
|
||||
{ label: 'Keywords', path: '#keywords', icon: <BoltIcon className="w-4 h-4" /> },
|
||||
];
|
||||
|
||||
// Handle sector toggle
|
||||
const handleSectorToggle = (sectorSlug: string) => {
|
||||
setSelectedSectors(prev =>
|
||||
@@ -381,6 +389,7 @@ export default function IndustriesSectorsKeywords() {
|
||||
title="Industries, Sectors & Keywords"
|
||||
badge={{ icon: <PieChartIcon />, color: 'blue' }}
|
||||
hideSiteSector={true}
|
||||
navigation={<ModuleNavigationTabs tabs={setupTabs} />}
|
||||
/>
|
||||
|
||||
<div className="p-6">
|
||||
|
||||
@@ -130,8 +130,7 @@ export default function SiteContentManager() {
|
||||
hideSiteSector
|
||||
/>
|
||||
<div className="mb-6 flex justify-end">
|
||||
<Button onClick={() => navigate(`/sites/${siteId}/posts/new`)} variant="primary">
|
||||
<PlusIcon className="w-4 h-4 mr-2" />
|
||||
<Button onClick={() => navigate(`/sites/${siteId}/posts/new`)} variant="primary" startIcon={<PlusIcon className="w-4 h-4" />}>
|
||||
New Post
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import PageMeta from '../../components/common/PageMeta';
|
||||
import PageHeader from '../../components/common/PageHeader';
|
||||
import ComponentCard from '../../components/common/ComponentCard';
|
||||
import { Card } from '../../components/ui/card';
|
||||
import Button from '../../components/ui/button/Button';
|
||||
import EnhancedMetricCard from '../../components/dashboard/EnhancedMetricCard';
|
||||
@@ -204,8 +205,8 @@ export default function SiteDashboard() {
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => navigate(`/sites/${siteId}/preview`)}
|
||||
startIcon={<EyeIcon className="w-4 h-4" />}
|
||||
>
|
||||
<EyeIcon className="w-4 h-4 mr-2" />
|
||||
Preview
|
||||
</Button>
|
||||
<Button
|
||||
@@ -245,11 +246,8 @@ export default function SiteDashboard() {
|
||||
</div>
|
||||
|
||||
|
||||
{/* Quick Actions - Matching Planner Dashboard pattern */}
|
||||
<div className="mb-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Quick Actions
|
||||
</h2>
|
||||
{/* Quick Actions */}
|
||||
<ComponentCard title="Quick Actions" desc="Common site management tasks">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<button
|
||||
onClick={() => navigate(`/sites/${siteId}/pages`)}
|
||||
@@ -321,7 +319,7 @@ export default function SiteDashboard() {
|
||||
<ArrowRightIcon className="h-5 w-5 text-slate-400 group-hover:text-[var(--color-primary)] transition" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
|
||||
{/* Recent Activity */}
|
||||
<Card className="p-6">
|
||||
|
||||
@@ -389,8 +389,8 @@ export default function DeploymentPanel() {
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => loadReadiness(selectedBlueprintId!)}
|
||||
startIcon={<BoltIcon className="w-4 h-4" />}
|
||||
>
|
||||
<BoltIcon className="w-4 h-4 mr-2" />
|
||||
Refresh Checks
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
@@ -571,43 +571,39 @@ export default function SiteList() {
|
||||
<div className="border-t border-gray-200 p-5 dark:border-gray-800">
|
||||
<div className="grid grid-cols-3 gap-2 mb-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => navigate(`/sites/${site.id}`)}
|
||||
className="w-full justify-center text-xs"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
startIcon={<EyeIcon className="w-4 h-4" />}
|
||||
>
|
||||
<EyeIcon className="w-3 h-3 mr-1" />
|
||||
Dashboard
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => navigate(`/sites/${site.id}/content`)}
|
||||
className="w-full justify-center text-xs"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
startIcon={<FileIcon className="w-4 h-4" />}
|
||||
>
|
||||
<FileIcon className="w-3 h-3 mr-1" />
|
||||
Content
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => navigate(`/sites/${site.id}/pages`)}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => navigate(`/sites/${site.id}/pages`)}
|
||||
className="w-full justify-center text-xs"
|
||||
startIcon={<PageIcon className="w-4 h-4" />}
|
||||
>
|
||||
<PageIcon className="w-3 h-3 mr-1" />
|
||||
Pages
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={() => navigate(`/sites/${site.id}/settings`)}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => navigate(`/sites/${site.id}/settings`)}
|
||||
title="Site Settings"
|
||||
startIcon={<PlugInIcon className="w-4 h-4" />}
|
||||
>
|
||||
<PlugInIcon className="w-4 h-4 mr-1" />
|
||||
<span className="text-xs">Settings</span>
|
||||
Settings
|
||||
</Button>
|
||||
</div>
|
||||
<Switch
|
||||
@@ -649,11 +645,9 @@ export default function SiteList() {
|
||||
title="Sites Management"
|
||||
badge={{ icon: <GridIcon />, color: 'blue' }}
|
||||
hideSiteSector={true}
|
||||
navigation={<ModuleNavigationTabs tabs={sitesTabs} />}
|
||||
/>
|
||||
|
||||
{/* In-page navigation tabs */}
|
||||
<ModuleNavigationTabs tabs={sitesTabs} />
|
||||
|
||||
{/* Info Alert */}
|
||||
<div className="mb-6">
|
||||
<Alert
|
||||
@@ -667,39 +661,29 @@ export default function SiteList() {
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex-1"></div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button onClick={() => navigate('/sites/builder')} variant="outline">
|
||||
<PlusIcon className="w-4 h-4 mr-2" />
|
||||
<Button onClick={() => navigate('/sites/builder')} variant="outline" startIcon={<PlusIcon className="w-4 h-4" />}>
|
||||
Create with Builder
|
||||
</Button>
|
||||
<Button onClick={handleCreateSite} variant="primary">
|
||||
<PlusIcon className="w-4 h-4 mr-2" />
|
||||
<Button onClick={handleCreateSite} variant="primary" startIcon={<PlusIcon className="w-4 h-4" />}>
|
||||
Add Site
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
<Button
|
||||
onClick={() => setViewType('table')}
|
||||
className={`inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
|
||||
viewType === 'table'
|
||||
? 'bg-white text-gray-900 dark:bg-gray-800 dark:text-white shadow-sm border border-gray-200 dark:border-gray-700'
|
||||
: 'text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
|
||||
}`}
|
||||
title="Table View"
|
||||
variant={viewType === 'table' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
startIcon={<TableIcon className="w-4 h-4" />}
|
||||
>
|
||||
<TableIcon className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">Table</span>
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setViewType('grid')}
|
||||
className={`inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
|
||||
viewType === 'grid'
|
||||
? 'bg-white text-gray-900 dark:bg-gray-800 dark:text-white shadow-sm border border-gray-200 dark:border-gray-700'
|
||||
: 'text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
|
||||
}`}
|
||||
title="Grid View"
|
||||
variant={viewType === 'grid' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
startIcon={<GridIcon className="w-4 h-4" />}
|
||||
>
|
||||
<GridIcon className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">Grid</span>
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -127,8 +127,7 @@ export default function SiteManagement() {
|
||||
Manage your sites, pages, and content
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={handleCreateSite} variant="primary">
|
||||
<PlusIcon className="w-4 h-4 mr-2" />
|
||||
<Button onClick={handleCreateSite} variant="primary" startIcon={<PlusIcon className="w-4 h-4" />}>
|
||||
Create New Site
|
||||
</Button>
|
||||
</div>
|
||||
@@ -182,8 +181,8 @@ export default function SiteManagement() {
|
||||
size="sm"
|
||||
onClick={() => handleIntegration(site.id)}
|
||||
className="w-full"
|
||||
startIcon={<PlugIcon className="w-4 h-4" />}
|
||||
>
|
||||
<PlugIcon className="w-4 h-4 mr-2" />
|
||||
{site.has_wordpress_integration ? 'Manage Integration' : 'Connect WordPress'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -286,8 +286,7 @@ export default function PageManager() {
|
||||
hideSiteSector
|
||||
/>
|
||||
<div className="mb-6 flex justify-end">
|
||||
<Button onClick={handleAddPage} variant="primary">
|
||||
<PlusIcon className="w-4 h-4 mr-2" />
|
||||
<Button onClick={handleAddPage} variant="primary" startIcon={<PlusIcon className="w-4 h-4" />}>
|
||||
Add Page
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -247,8 +247,8 @@ export default function PostEditor() {
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => navigate(`/sites/${siteId}/content`)}
|
||||
startIcon={<XIcon className="w-4 h-4" />}
|
||||
>
|
||||
<XIcon className="w-4 h-4 mr-2" />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
@@ -256,8 +256,8 @@ export default function PostEditor() {
|
||||
onClick={handleSave}
|
||||
disabled={saving || (content.status === 'publish' && validationResult && !validationResult.is_valid)}
|
||||
title={content.status === 'publish' && validationResult && !validationResult.is_valid ? 'Please fix validation errors before publishing' : undefined}
|
||||
startIcon={<SaveIcon className="w-4 h-4" />}
|
||||
>
|
||||
<SaveIcon className="w-4 h-4 mr-2" />
|
||||
{saving ? 'Saving...' : content.status === 'publish' ? 'Publish' : 'Save Post'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -179,16 +179,13 @@ export default function SitePreview() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={handleRefresh}>
|
||||
<RefreshCwIcon className="w-4 h-4 mr-2" />
|
||||
<Button variant="outline" onClick={handleRefresh} startIcon={<RefreshCwIcon className="w-4 h-4" />}>
|
||||
Refresh
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleOpenInNewTab}>
|
||||
<ExternalLinkIcon className="w-4 h-4 mr-2" />
|
||||
<Button variant="outline" onClick={handleOpenInNewTab} startIcon={<ExternalLinkIcon className="w-4 h-4" />}>
|
||||
Open in New Tab
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setIsFullscreen(true)}>
|
||||
<Maximize2Icon className="w-4 h-4 mr-2" />
|
||||
<Button variant="outline" onClick={() => setIsFullscreen(true)} startIcon={<Maximize2Icon className="w-4 h-4" />}>
|
||||
Fullscreen
|
||||
</Button>
|
||||
</div>
|
||||
@@ -197,8 +194,7 @@ export default function SitePreview() {
|
||||
|
||||
{isFullscreen && (
|
||||
<div className="absolute top-4 right-4 z-10">
|
||||
<Button variant="outline" onClick={() => setIsFullscreen(false)}>
|
||||
<Minimize2Icon className="w-4 h-4 mr-2" />
|
||||
<Button variant="outline" onClick={() => setIsFullscreen(false)} startIcon={<Minimize2Icon className="w-4 h-4" />}>
|
||||
Exit Fullscreen
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -400,8 +400,8 @@ export default function SyncDashboard() {
|
||||
size="sm"
|
||||
onClick={() => handleSync('both')}
|
||||
disabled={syncing}
|
||||
startIcon={<BoltIcon className="w-4 h-4" />}
|
||||
>
|
||||
<BoltIcon className="w-4 h-4 mr-2" />
|
||||
Retry Sync to Resolve
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -113,8 +113,8 @@ export default function AuthorProfiles() {
|
||||
<PageHeader
|
||||
title="Author Profiles"
|
||||
badge={{ icon: <UserIcon />, color: 'blue' }}
|
||||
navigation={<ModuleNavigationTabs tabs={thinkerTabs} />}
|
||||
/>
|
||||
<ModuleNavigationTabs tabs={thinkerTabs} />
|
||||
<div className="mb-6 flex justify-between items-center">
|
||||
<Button onClick={handleCreate} variant="primary">
|
||||
<PlusIcon className="w-4 h-4 mr-2" />
|
||||
|
||||
@@ -260,47 +260,56 @@ export default function ThinkerDashboard() {
|
||||
{/* Quick Actions */}
|
||||
<ComponentCard title="Quick Actions" desc="Create new prompts, profiles, or strategies">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<button
|
||||
<Button
|
||||
onClick={() => navigate("/thinker/prompts")}
|
||||
className="flex items-center gap-4 p-6 rounded-xl border-2 border-slate-200 bg-white hover:border-[var(--color-warning)] hover:shadow-lg transition-all group"
|
||||
variant="outline"
|
||||
size="lg"
|
||||
startIcon={
|
||||
<div className="size-8 rounded-lg bg-gradient-to-br from-[var(--color-warning)] to-[var(--color-warning-dark)] flex items-center justify-center text-white shadow-md">
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</div>
|
||||
}
|
||||
className="!justify-start !h-auto !py-4"
|
||||
>
|
||||
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-warning)] to-[var(--color-warning-dark)] flex items-center justify-center text-white shadow-lg">
|
||||
<PlusIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="flex-1 text-left">
|
||||
<h4 className="font-semibold text-slate-900 mb-1">New Prompt</h4>
|
||||
<p className="text-sm text-slate-600">Create a reusable prompt template</p>
|
||||
</div>
|
||||
<ArrowRightIcon className="h-5 w-5 text-slate-400 group-hover:text-[#ff7a00] transition" />
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
<button
|
||||
<Button
|
||||
onClick={() => navigate("/thinker/profiles")}
|
||||
className="flex items-center gap-4 p-6 rounded-xl border-2 border-slate-200 bg-white hover:border-[#0693e3] hover:shadow-lg transition-all group"
|
||||
variant="outline"
|
||||
size="lg"
|
||||
startIcon={
|
||||
<div className="size-8 rounded-lg bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] flex items-center justify-center text-white shadow-md">
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</div>
|
||||
}
|
||||
className="!justify-start !h-auto !py-4"
|
||||
>
|
||||
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] flex items-center justify-center text-white shadow-lg">
|
||||
<PlusIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="flex-1 text-left">
|
||||
<h4 className="font-semibold text-slate-900 mb-1">New Author Profile</h4>
|
||||
<p className="text-sm text-slate-600">Define a writing voice and style</p>
|
||||
</div>
|
||||
<ArrowRightIcon className="h-5 w-5 text-slate-400 group-hover:text-[var(--color-primary)] transition" />
|
||||
</button>
|
||||
</Button>
|
||||
|
||||
<button
|
||||
<Button
|
||||
onClick={() => navigate("/thinker/strategies")}
|
||||
className="flex items-center gap-4 p-6 rounded-xl border-2 border-slate-200 bg-white hover:border-[#5d4ae3] hover:shadow-lg transition-all group"
|
||||
variant="outline"
|
||||
size="lg"
|
||||
startIcon={
|
||||
<div className="size-8 rounded-lg bg-gradient-to-br from-[var(--color-purple)] to-[var(--color-purple-dark)] flex items-center justify-center text-white shadow-md">
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</div>
|
||||
}
|
||||
className="!justify-start !h-auto !py-4"
|
||||
>
|
||||
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-purple)] to-[var(--color-purple-dark)] flex items-center justify-center text-white shadow-lg">
|
||||
<PlusIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="flex-1 text-left">
|
||||
<h4 className="font-semibold text-slate-900 mb-1">New Strategy</h4>
|
||||
<p className="text-sm text-slate-600">Build a content playbook</p>
|
||||
</div>
|
||||
<ArrowRightIcon className="h-5 w-5 text-slate-400 group-hover:text-[#5d4ae3] transition" />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ export default function ImageTesting() {
|
||||
<PageHeader
|
||||
title="Image Testing"
|
||||
badge={{ icon: <ImageIcon />, color: 'indigo' }}
|
||||
navigation={<ModuleNavigationTabs tabs={thinkerTabs} />}
|
||||
/>
|
||||
<ModuleNavigationTabs tabs={thinkerTabs} />
|
||||
<ComponentCard title="Coming Soon" desc="AI image testing">
|
||||
<div className="text-center py-8">
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
|
||||
@@ -213,8 +213,8 @@ export default function Prompts() {
|
||||
<PageHeader
|
||||
title="AI Prompts Management"
|
||||
badge={{ icon: <BoltIcon />, color: 'orange' }}
|
||||
navigation={<ModuleNavigationTabs tabs={thinkerTabs} />}
|
||||
/>
|
||||
<ModuleNavigationTabs tabs={thinkerTabs} />
|
||||
<div className="p-6">
|
||||
|
||||
{/* Planner Prompts Section */}
|
||||
@@ -265,16 +265,7 @@ export default function Prompts() {
|
||||
placeholder="Enter prompt template..."
|
||||
className="font-mono-custom text-sm"
|
||||
/>
|
||||
<div className="flex gap-3 mt-4">
|
||||
<Button
|
||||
onClick={() => handleSave(type.key)}
|
||||
disabled={saving[type.key]}
|
||||
className="flex-1"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
>
|
||||
{saving[type.key] ? 'Saving...' : 'Save Prompt'}
|
||||
</Button>
|
||||
<div className="flex justify-end gap-3 mt-4">
|
||||
<Button
|
||||
onClick={() => handleReset(type.key)}
|
||||
disabled={saving[type.key]}
|
||||
@@ -282,6 +273,13 @@ export default function Prompts() {
|
||||
>
|
||||
Reset to Default
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleSave(type.key)}
|
||||
disabled={saving[type.key]}
|
||||
variant="primary"
|
||||
>
|
||||
{saving[type.key] ? 'Saving...' : 'Save Prompt'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -337,16 +335,7 @@ export default function Prompts() {
|
||||
placeholder="Enter prompt template..."
|
||||
className="font-mono-custom text-sm"
|
||||
/>
|
||||
<div className="flex gap-3 mt-4">
|
||||
<Button
|
||||
onClick={() => handleSave(type.key)}
|
||||
disabled={saving[type.key]}
|
||||
className="flex-1"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
>
|
||||
{saving[type.key] ? 'Saving...' : 'Save Prompt'}
|
||||
</Button>
|
||||
<div className="flex justify-end gap-3 mt-4">
|
||||
<Button
|
||||
onClick={() => handleReset(type.key)}
|
||||
disabled={saving[type.key]}
|
||||
@@ -354,6 +343,13 @@ export default function Prompts() {
|
||||
>
|
||||
Reset to Default
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleSave(type.key)}
|
||||
disabled={saving[type.key]}
|
||||
variant="primary"
|
||||
>
|
||||
{saving[type.key] ? 'Saving...' : 'Save Prompt'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -409,16 +405,7 @@ export default function Prompts() {
|
||||
placeholder="Enter prompt template..."
|
||||
className="font-mono-custom text-sm"
|
||||
/>
|
||||
<div className="flex gap-3 mt-4">
|
||||
<Button
|
||||
onClick={() => handleSave(type.key)}
|
||||
disabled={saving[type.key]}
|
||||
className="flex-1"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
>
|
||||
{saving[type.key] ? 'Saving...' : 'Save Prompt'}
|
||||
</Button>
|
||||
<div className="flex justify-end gap-3 mt-4">
|
||||
{type.key === 'image_prompt_template' && (
|
||||
<Button
|
||||
onClick={() => handleReset(type.key)}
|
||||
@@ -428,6 +415,13 @@ export default function Prompts() {
|
||||
Reset to Default
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => handleSave(type.key)}
|
||||
disabled={saving[type.key]}
|
||||
variant="primary"
|
||||
>
|
||||
{saving[type.key] ? 'Saving...' : 'Save Prompt'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -492,16 +486,7 @@ export default function Prompts() {
|
||||
placeholder="Enter prompt template for site structure generation..."
|
||||
className="font-mono-custom text-sm"
|
||||
/>
|
||||
<div className="flex gap-3 mt-4">
|
||||
<Button
|
||||
onClick={() => handleSave(type.key)}
|
||||
disabled={saving[type.key]}
|
||||
className="flex-1"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
>
|
||||
{saving[type.key] ? 'Saving...' : 'Save Prompt'}
|
||||
</Button>
|
||||
<div className="flex justify-end gap-3 mt-4">
|
||||
<Button
|
||||
onClick={() => handleReset(type.key)}
|
||||
disabled={saving[type.key]}
|
||||
@@ -509,6 +494,13 @@ export default function Prompts() {
|
||||
>
|
||||
Reset to Default
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleSave(type.key)}
|
||||
disabled={saving[type.key]}
|
||||
variant="primary"
|
||||
>
|
||||
{saving[type.key] ? 'Saving...' : 'Save Prompt'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,8 +19,8 @@ export default function Strategies() {
|
||||
<PageHeader
|
||||
title="Content Strategies"
|
||||
badge={{ icon: <ShootingStarIcon />, color: 'purple' }}
|
||||
navigation={<ModuleNavigationTabs tabs={thinkerTabs} />}
|
||||
/>
|
||||
<ModuleNavigationTabs tabs={thinkerTabs} />
|
||||
<ComponentCard title="Coming Soon" desc="Content strategies">
|
||||
<div className="text-center py-8">
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
|
||||
@@ -246,8 +246,8 @@ export default function Content() {
|
||||
<PageHeader
|
||||
title="Content"
|
||||
badge={{ icon: <FileIcon />, color: 'purple' }}
|
||||
navigation={<ModuleNavigationTabs tabs={writerTabs} />}
|
||||
/>
|
||||
<ModuleNavigationTabs tabs={writerTabs} />
|
||||
<TablePageTemplate
|
||||
columns={pageConfig.columns}
|
||||
data={content}
|
||||
|
||||
@@ -474,8 +474,8 @@ export default function Images() {
|
||||
<PageHeader
|
||||
title="Content Images"
|
||||
badge={{ icon: <FileIcon />, color: 'orange' }}
|
||||
navigation={<ModuleNavigationTabs tabs={writerTabs} />}
|
||||
/>
|
||||
<ModuleNavigationTabs tabs={writerTabs} />
|
||||
<TablePageTemplate
|
||||
columns={pageConfig.columns}
|
||||
data={images}
|
||||
|
||||
@@ -571,8 +571,8 @@ export default function Tasks() {
|
||||
<PageHeader
|
||||
title="Tasks"
|
||||
badge={{ icon: <TaskIcon />, color: 'indigo' }}
|
||||
navigation={<ModuleNavigationTabs tabs={writerTabs} />}
|
||||
/>
|
||||
<ModuleNavigationTabs tabs={writerTabs} />
|
||||
<TablePageTemplate
|
||||
columns={pageConfig.columns}
|
||||
data={tasks}
|
||||
|
||||
@@ -231,6 +231,18 @@ export async function fetchAPI(endpoint: string, options?: RequestInit & { timeo
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} else {
|
||||
// Retry failed - parse and throw the retry error (not the original 401)
|
||||
let retryError: any = new Error(retryResponse.statusText);
|
||||
retryError.status = retryResponse.status;
|
||||
try {
|
||||
const retryErrorData = JSON.parse(retryText);
|
||||
retryError.message = retryErrorData.error || retryErrorData.message || retryResponse.statusText;
|
||||
retryError.data = retryErrorData;
|
||||
} catch (e) {
|
||||
retryError.message = retryText.substring(0, 200) || retryResponse.statusText;
|
||||
}
|
||||
throw retryError;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -238,6 +250,7 @@ export async function fetchAPI(endpoint: string, options?: RequestInit & { timeo
|
||||
// Refresh failed, clear auth state and force re-login
|
||||
const { logout } = useAuthStore.getState();
|
||||
logout();
|
||||
throw refreshError;
|
||||
}
|
||||
} else {
|
||||
// No refresh token available, clear auth state
|
||||
|
||||
@@ -235,12 +235,11 @@ export const useAuthStore = create<AuthState>()(
|
||||
|
||||
set({ user: refreshedUser, isAuthenticated: true });
|
||||
} catch (error: any) {
|
||||
// Only logout on authentication/authorization errors, not on network errors
|
||||
// Network errors (500, timeout, etc.) should not log the user out
|
||||
// Only logout on specific authentication/authorization errors
|
||||
// Do NOT logout on 401/403 - fetchAPI handles token refresh automatically
|
||||
// A 401 that reaches here means refresh token is invalid, which fetchAPI handles by logging out
|
||||
const isAuthError = error?.code === 'ACCOUNT_REQUIRED' ||
|
||||
error?.code === 'PLAN_REQUIRED' ||
|
||||
error?.status === 401 ||
|
||||
error?.status === 403 ||
|
||||
(error?.message && error.message.includes('Not authenticated'));
|
||||
|
||||
if (isAuthError) {
|
||||
@@ -248,7 +247,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
console.warn('Authentication error during refresh, logging out:', error);
|
||||
set({ user: null, token: null, refreshToken: null, isAuthenticated: false });
|
||||
} else {
|
||||
// Network/server error - don't logout, just throw the error
|
||||
// Network/server error or 401/403 (handled by fetchAPI) - don't logout
|
||||
// The caller (AppLayout) will handle it gracefully
|
||||
console.debug('Non-auth error during refresh (will retry):', error);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user