ui frotneedn fixes

This commit is contained in:
IGNY8 VPS (Salman)
2025-11-26 06:47:23 +00:00
parent 451594bd29
commit 4fe68cc271
40 changed files with 1638 additions and 275 deletions

View File

@@ -0,0 +1,225 @@
# Button Component Standardization - Complete
## Overview
Fixed Button component and all Sites pages to match IGNY8 global design standard:
- Secondary button color now uses `text-dim` (#64748b)
- Removed ALL `Link` component usage from buttons
- All buttons now use `onClick` navigation with `useNavigate()`
## Changes Made
### 1. Button Component (`/frontend/src/components/ui/button/Button.tsx`)
#### Removed Link/Anchor Support
- **Removed imports**: `Link` from react-router-dom
- **Removed props**: `as`, `href`, `to`, `target`, `rel`
- **Simplified component**: Always renders `<button>` element (no conditional rendering)
- **Result**: Button component ONLY supports `onClick` navigation
#### Fixed Secondary Color
Changed ALL secondary variants to use text-dim color (#64748b):
**Before (WRONG):**
```tsx
brand: {
secondary: "bg-brand-50 text-brand-600 hover:bg-brand-100"
}
success: {
secondary: "bg-success-50 text-success-600 hover:bg-success-100"
}
// etc...
```
**After (CORRECT):**
```tsx
brand: {
secondary: "bg-transparent text-[#64748b] hover:bg-gray-100 dark:hover:bg-white/[0.08]"
}
success: {
secondary: "bg-transparent text-[#64748b] hover:bg-gray-100 dark:hover:bg-white/[0.08]"
}
// etc...
```
**Affected tones:**
- ✅ brand
- ✅ success
- ✅ warning
- ✅ danger
- ✅ neutral (already correct)
### 2. Sites Pages - Removed All Link Usage
#### Dashboard.tsx (`/frontend/src/pages/Sites/Dashboard.tsx`)
- **Removed import**: `Link` from react-router-dom
- **Changed**: 5 Quick Action cards from `<Link to={...}>` to `<button onClick={() => navigate(...)}>>`
- **Actions updated**:
- Manage Pages
- Manage Content
- Integrations
- Sync Dashboard
- Deploy Site
**Before:**
```tsx
<Link to={`/sites/${siteId}/pages`} className="...">
{/* card content */}
</Link>
```
**After:**
```tsx
<button onClick={() => navigate(`/sites/${siteId}/pages`)} className="...">
{/* card content */}
</button>
```
#### List.tsx (`/frontend/src/pages/Sites/List.tsx`)
- **Removed import**: `Link` from react-router-dom
- **Changed**: Site card buttons from `Button as={Link} to={...}` to `Button onClick={() => navigate(...)}`
- **Buttons updated**:
- Dashboard (primary variant)
- Content (secondary variant)
- Pages (secondary variant)
- Settings (secondary variant)
**Before:**
```tsx
<Button
as={Link}
to={`/sites/${site.id}`}
variant="primary"
size="sm"
startIcon={<EyeIcon className="w-4 h-4" />}
>
Dashboard
</Button>
```
**After:**
```tsx
<Button
onClick={() => navigate(`/sites/${site.id}`)}
variant="primary"
size="sm"
startIcon={<EyeIcon className="w-4 h-4" />}
>
Dashboard
</Button>
```
#### Content.tsx (`/frontend/src/pages/Sites/Content.tsx`)
- **Removed import**: `Link` from react-router-dom
- **Changed**: 2 buttons from `Button as={Link}` to `Button onClick`
- **Buttons updated**:
- "New Post" button (top of page)
- "Create Your First Post" button (empty state)
#### Editor.tsx (`/frontend/src/pages/Sites/Editor.tsx`)
- **Removed import**: `Link` from react-router-dom (was imported but not yet used)
## Verification
### TypeScript Compilation
✅ No errors found in:
- `/frontend/src/components/ui/button/Button.tsx`
- `/frontend/src/pages/Sites/Dashboard.tsx`
- `/frontend/src/pages/Sites/List.tsx`
- `/frontend/src/pages/Sites/Content.tsx`
- `/frontend/src/pages/Sites/Editor.tsx`
### Code Search
✅ No remaining instances of:
- `<Button as={Link}` in Sites pages
- `Button.*as={Link}` anywhere in frontend
## Button Standard Summary
### Correct Usage (ONLY allowed format)
```tsx
import { Button } from '@/components/ui/button/Button';
import { useNavigate } from 'react-router-dom';
const navigate = useNavigate();
// Simple button
<Button onClick={() => navigate('/path')} variant="primary">
Click Me
</Button>
// Button with icon
<Button
onClick={() => navigate('/path')}
variant="secondary"
startIcon={<IconComponent className="w-4 h-4" />}
>
Action
</Button>
// Multiple buttons (primary + secondary)
<div className="flex gap-2">
<Button onClick={handlePrimary} variant="primary">
Primary Action
</Button>
<Button onClick={handleSecondary} variant="secondary">
Secondary Action
</Button>
</div>
```
### Variants
- **primary**: Brand color background (#0693e3), white text
- **secondary**: Text-dim color (#64748b), transparent background, gray hover
- **outline**: Colored text with ring border
- **ghost**: Colored text with subtle hover
- **gradient**: Gradient background with shadow
### Tones
- **brand**: Primary blue (#0693e3)
- **success**: Teal-green (#0bbf87)
- **warning**: Vivid orange (#ff7a00)
- **danger**: Red (#ef4444)
- **neutral**: Gray
### Sizes
- **xs**: h-7 px-2.5 text-xs
- **sm**: h-9 px-3 text-sm (most common)
- **md**: h-10 px-4 text-sm
- **lg**: h-12 px-5 text-base
## Color Reference
### Text-Dim Color
```css
/* tokens.css */
--color-text-dim: #64748b; /* Light mode */
.dark {
--color-text-dim: #9ca3af; /* Dark mode */
}
```
This is Tailwind's `slate-500` equivalent, used for:
- Secondary button text
- Subdued/helper text
- Less prominent UI elements
## What Was Wrong Before
1. **Secondary color mismatch**: Used tone-specific colors (brand-600, success-600, etc.) instead of universal text-dim
2. **Link component usage**: Button component had `as={Link}` prop support (accessibility anti-pattern for navigation buttons)
3. **Mixed patterns**: Some pages used `<Link>`, others used `Button as={Link}`, creating inconsistency
4. **Wrong variants**: Some buttons still used old "solid"/"soft" names
## Result
**Consistent design**: All secondary buttons now have same text-dim color across all tones
**Single pattern**: ALL buttons use `onClick` with `useNavigate()` - no Link components
**Proper semantics**: Navigation uses button elements with click handlers (matches HTML standard)
**Type safe**: Removed unnecessary props from Button component
**Maintainable**: Single source of truth for button styles in Button.tsx
---
**Date**: 2024
**Status**: ✅ Complete - All Sites pages standardized

515
IGNY8_DESIGN_STANDARD.md Normal file
View File

@@ -0,0 +1,515 @@
# IGNY8 Design Standard Reference
**Standardized UI patterns used across Planner, Writer, and Dashboard modules**
This document defines the locked design patterns and component usage standards for the IGNY8 application. All modules (including Sites) should follow these patterns to maintain visual consistency.
---
## Core Component Library
### 1. Button Component
**Location:** `frontend/src/components/ui/button/Button.tsx`
**Status:** 🔒 STYLE LOCKED - See `DESIGN_SYSTEM.md`
#### Variants (5 total)
- `solid` - Filled background (primary action)
- `soft` - Light background (secondary action)
- `outline` - Border only (tertiary action)
- `ghost` - No border or background (minimal action)
- `gradient` - Gradient background with shadow (premium/highlight action)
#### Sizes (4 total)
- `xs` - Extra small
- `sm` - Small
- `md` - Medium (default)
- `lg` - Large
#### Tones (5 total)
- `brand` - Primary brand color (blue)
- `success` - Green
- `warning` - Orange
- `danger` - Red/Error
- `neutral` - Gray
#### Usage Example
```tsx
import Button from '../../components/ui/button/Button';
<Button variant="solid" tone="brand" size="md" startIcon={<Icon />}>
Click Me
</Button>
```
#### ⚠️ Anti-Pattern
```tsx
// ❌ DON'T: Raw HTML buttons with inline Tailwind
<button className="px-4 py-2 bg-blue-500 text-white rounded-lg">
Click Me
</button>
// ✅ DO: Use Button component
<Button variant="solid" tone="brand">
Click Me
</Button>
```
---
### 2. ComponentCard
**Location:** `frontend/src/components/common/ComponentCard.tsx`
**Purpose:** Standard card wrapper for sections with title and description
#### Props
- `title` (required) - Section title (string or ReactNode)
- `desc` (optional) - Description text below title
- `children` (required) - Card content
- `className` (optional) - Additional styling
#### Usage Example
```tsx
import ComponentCard from '../../components/common/ComponentCard';
<ComponentCard title="Quick Actions" desc="Common planning tasks and shortcuts">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Content */}
</div>
</ComponentCard>
```
#### Structure
- **Header:** Title + optional description (gray text)
- **Body:** Border-top separated content area with padding
- **Dark mode:** Automatic theme support
#### ⚠️ Anti-Pattern
```tsx
// ❌ DON'T: Raw Card component with manual header
<Card>
<div className="px-6 py-5">
<h3 className="text-base font-medium">Quick Actions</h3>
</div>
<div className="p-6">
{/* Content */}
</div>
</Card>
// ✅ DO: Use ComponentCard
<ComponentCard title="Quick Actions">
{/* Content */}
</ComponentCard>
```
---
### 3. EnhancedMetricCard
**Location:** `frontend/src/components/dashboard/EnhancedMetricCard.tsx`
**Purpose:** Display metrics with optional trends, tooltips, and navigation
#### Key Props
- `title` (required) - Metric label
- `value` (required) - Main metric value (string | number)
- `subtitle` (optional) - Additional context below value
- `icon` (optional) - Icon component
- `accentColor` (required) - Border accent color
- `trend` (optional) - { direction: 'up' | 'down', value: string }
- `href` (optional) - React Router navigation path
- `onClick` (optional) - Click handler (alternative to href)
- `tooltip` (optional) - Tooltip text on hover
- `details` (optional) - Array of tooltip detail breakdowns
#### Accent Colors (6 total)
- `blue` - Primary/default metrics
- `green` - Success/positive metrics
- `orange` - Warning/attention metrics
- `purple` - Special/premium metrics
- `red` - Error/critical metrics
- `success` - Alternative green (var(--color-success))
#### Usage Example
```tsx
import EnhancedMetricCard from '../../components/dashboard/EnhancedMetricCard';
<EnhancedMetricCard
title="Active Keywords"
value={1234}
subtitle="Across all clusters"
icon={<ListIcon className="h-5 w-5" />}
accentColor="blue"
trend={{ direction: 'up', value: '+12%' }}
href="/planner/keywords"
tooltip="View all active keywords"
details={[
{ label: 'Tracked', value: '800' },
{ label: 'Untracked', value: '434' },
]}
/>
```
#### Features
- Automatic Link wrapping when `href` provided
- Hover effects and transitions
- Dark mode support
- Tooltip with optional details breakdown
- Trend indicators with arrows
#### ⚠️ Anti-Pattern
```tsx
// ❌ DON'T: Custom metric cards with inline styles
<div className="bg-white rounded-xl border-2 border-slate-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-gray-600">Active Keywords</p>
<p className="text-3xl font-bold">1,234</p>
</div>
<ListIcon className="h-8 w-8 text-blue-500" />
</div>
</div>
// ✅ DO: Use EnhancedMetricCard
<EnhancedMetricCard
title="Active Keywords"
value={1234}
icon={<ListIcon className="h-5 w-5" />}
accentColor="blue"
/>
```
---
### 4. PageHeader
**Location:** `frontend/src/components/common/PageHeader.tsx`
**Purpose:** Standardized page header with title, site/sector info, and selectors
#### Key Props
- `title` (required) - Page title
- `lastUpdated` (optional) - Last refresh timestamp
- `badge` (optional) - { icon: ReactNode, color: 'blue' | 'green' | ... }
- `showRefresh` (optional) - Show refresh button
- `onRefresh` (optional) - Refresh button handler
- `hideSiteSector` (optional) - Hide site/sector info for global pages
- `className` (optional) - Additional styling
#### Badge Colors (6 total)
Same as Button/Metric: `blue`, `green`, `purple`, `orange`, `red`, `indigo`
#### Usage Example
```tsx
import PageHeader from '../../components/common/PageHeader';
import { ListIcon } from '../../icons';
<PageHeader
title="Keyword Dashboard"
lastUpdated={new Date()}
badge={{ icon: <ListIcon className="h-5 w-5" />, color: 'blue' }}
showRefresh={true}
onRefresh={handleRefresh}
/>
```
#### Features
- Automatic site/sector display from stores
- SiteAndSectorSelector integration
- Responsive layout (stack on mobile)
- Badge with icon support
- Last updated timestamp
#### ⚠️ Anti-Pattern
```tsx
// ❌ DON'T: Custom page headers with manual layout
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-2xl font-bold">Keyword Dashboard</h2>
<p className="text-sm text-gray-500">
Site: {site.name} Sector: {sector.name}
</p>
</div>
<button onClick={refresh}>Refresh</button>
</div>
// ✅ DO: Use PageHeader
<PageHeader
title="Keyword Dashboard"
showRefresh={true}
onRefresh={refresh}
/>
```
---
### 5. Link Component (React Router)
**Location:** `react-router-dom`
**Purpose:** Standard navigation with automatic prefetching and accessibility
#### Usage Example
```tsx
import { Link } from 'react-router-dom';
<Link
to="/planner/keywords"
className="flex items-center gap-4 p-6 rounded-xl border-2 border-slate-200 bg-white hover:border-[var(--color-primary)] hover:shadow-lg transition-all group"
>
<div>Navigate to Keywords</div>
</Link>
```
#### Benefits Over Button + Navigate
- ✅ Proper semantic HTML (`<a>` tag)
- ✅ Keyboard navigation (Tab + Enter)
- ✅ Right-click "Open in new tab" support
- ✅ Screen reader accessibility
- ✅ Browser history support
- ✅ Automatic prefetching
#### ⚠️ Anti-Pattern
```tsx
// ❌ DON'T: Button with onClick navigate
<button onClick={() => navigate('/planner/keywords')}>
Go to Keywords
</button>
// ✅ DO: Use Link component
<Link to="/planner/keywords">
Go to Keywords
</Link>
```
---
## Design Patterns
### Quick Actions Grid
**Standard pattern used in:** Planner Dashboard, Writer Dashboard
#### Structure
```tsx
<ComponentCard title="Quick Actions" desc="Common planning tasks and shortcuts">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Link
to="/path"
className="flex items-center gap-4 p-6 rounded-xl border-2 border-slate-200 bg-white hover:border-[var(--color-primary)] hover:shadow-lg transition-all group"
>
{/* Gradient Icon */}
<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">
<Icon className="h-6 w-6" />
</div>
{/* Content */}
<div className="flex-1 text-left">
<h4 className="font-semibold text-slate-900 mb-1">Action Title</h4>
<p className="text-sm text-slate-600">Action description</p>
</div>
{/* Arrow Icon */}
<ArrowRightIcon className="h-5 w-5 text-slate-400 group-hover:text-[var(--color-primary)] transition" />
</Link>
</div>
</ComponentCard>
```
#### Key Elements
1. **ComponentCard wrapper** - Title + description
2. **Responsive grid** - 1 col mobile, 2 col tablet, 4 col desktop
3. **Link component** - Not button
4. **Gradient icon box** - 48px (size-12), gradient from primary to primary-dark
5. **Content area** - Title (font-semibold) + description (text-sm)
6. **Arrow icon** - Right-pointing, changes color on hover
7. **Hover effects** - Border color + shadow on hover
#### Gradient Color Variants
```tsx
// Primary (Blue)
from-[var(--color-primary)] to-[var(--color-primary-dark)]
// Success (Green)
from-[var(--color-success)] to-[var(--color-success-dark)]
// Warning (Orange)
from-[var(--color-warning)] to-[var(--color-warning-dark)]
// Purple
from-[var(--color-purple)] to-[var(--color-purple-dark)]
```
#### ⚠️ Anti-Pattern - Sites Dashboard Current Implementation
```tsx
// ❌ DON'T: Button with navigate + manual styling
<button
onClick={() => navigate(`/sites/${siteId}/pages`)}
className="flex items-center gap-4 p-6 rounded-xl border-2 border-slate-200 bg-white hover:border-[var(--color-primary)] hover:shadow-lg transition-all group"
>
{/* ... */}
</button>
// ✅ DO: Link component
<Link
to={`/sites/${siteId}/pages`}
className="flex items-center gap-4 p-6 rounded-xl border-2 border-slate-200 bg-white hover:border-[var(--color-primary)] hover:shadow-lg transition-all group"
>
{/* ... */}
</Link>
```
---
### Metrics Dashboard Grid
**Standard pattern used in:** Planner Dashboard, Writer Dashboard
#### Structure
```tsx
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<EnhancedMetricCard
title="Metric Name"
value={1234}
icon={<Icon className="h-5 w-5" />}
accentColor="blue"
href="/path"
/>
</div>
```
#### Grid Breakpoints
- **Mobile (< 768px):** 1 column
- **Tablet (768px - 1024px):** 2 columns
- **Desktop (> 1024px):** 3 columns
#### Best Practices
- Use `href` prop for navigation (not `onClick`)
- Consistent icon sizing: `h-5 w-5`
- Map accent colors to metric meaning (blue = neutral, green = success, orange = warning, red = error)
- Include tooltips for complex metrics
- Add trend indicators when comparing periods
---
## Color System
### CSS Variables
```css
--color-primary: #0693e3; /* Brand Blue */
--color-primary-dark: #0570b8;
--color-success: #0bbf87; /* Green */
--color-success-dark: #089968;
--color-warning: #ff7a00; /* Orange */
--color-warning-dark: #cc6200;
--color-purple: #5d4ae3;
--color-purple-dark: #4a3bb5;
--color-error: #f44336; /* Red */
```
### Tailwind Color Classes
- `brand-*` - Primary blue (50-900)
- `success-*` - Green (50-900)
- `warning-*` - Orange (50-900)
- `error-*` - Red (50-900)
- `purple-*` - Purple (50-900)
- `gray-*` - Neutral (50-900)
---
## Sites Module Refactor Checklist
### Current Inconsistencies (Sites Dashboard Example)
- ❌ Uses `<button onClick={() => navigate(...)}` instead of `<Link to={...}>`
- ❌ Missing ComponentCard wrapper for Quick Actions section
- ❌ Manual heading instead of ComponentCard title prop
- ⚠️ Uses Button component correctly (partial compliance)
- ✅ Uses EnhancedMetricCard correctly
### Required Changes
1. **Replace all `<button onClick={() => navigate(...)}` with `<Link to={...}>`**
- Better accessibility
- Standard keyboard navigation
- Consistent with Planner/Writer modules
2. **Wrap Quick Actions in ComponentCard**
- Current: Manual `<h2>` heading
- Target: `<ComponentCard title="Quick Actions" desc="...">`
3. **Extract ActionCard component (if repeated)**
- DRY principle for Quick Action cards
- Reusable across Sites module
4. **Standardize Button usage**
- Verify all buttons use `variant` prop (not custom classes)
- Ensure consistent tone/size across module
5. **Add missing EnhancedMetricCard features**
- Tooltips for complex metrics
- Trend indicators where applicable
---
## Implementation Priority
### Phase 1: Navigation (High Impact)
1. Replace `button + navigate` with `Link` components
2. Update click handlers to href props
3. Test keyboard navigation and accessibility
### Phase 2: Component Wrapping (Medium Impact)
1. Wrap sections in ComponentCard
2. Replace manual headings with ComponentCard title prop
3. Verify consistent spacing and styling
### Phase 3: Component Extraction (Low Impact)
1. Create reusable ActionCard component
2. Create SiteMetricCard if Sites-specific logic needed
3. Update DESIGN_SYSTEM.md with new components
### Phase 4: Polish (Continuous)
1. Add missing tooltips
2. Add trend indicators
3. Verify dark mode consistency
4. Test responsive layouts
---
## Testing Checklist
### Visual Consistency
- [ ] Quick Actions match Planner/Writer pattern
- [ ] Metrics grid matches dashboard standards
- [ ] Button variants consistent across pages
- [ ] Color usage matches design system
### Accessibility
- [ ] All navigation uses Link (not button)
- [ ] Keyboard navigation works (Tab, Enter)
- [ ] Screen reader labels present
- [ ] Focus indicators visible
### Functionality
- [ ] All routes navigate correctly
- [ ] Hover states work consistently
- [ ] Dark mode renders properly
- [ ] Responsive breakpoints work
### Code Quality
- [ ] No raw `<button>` for navigation
- [ ] No inline Tailwind for common patterns
- [ ] TypeScript errors resolved
- [ ] Component props properly typed
---
## References
### Key Files
- `frontend/DESIGN_SYSTEM.md` - Locked component variants
- `frontend/src/components/ui/button/Button.tsx` - Button component
- `frontend/src/components/common/ComponentCard.tsx` - Card wrapper
- `frontend/src/components/dashboard/EnhancedMetricCard.tsx` - Metric display
- `frontend/src/components/common/PageHeader.tsx` - Page header
- `frontend/src/pages/Planner/Dashboard.tsx` - Reference implementation
- `frontend/src/pages/Writer/Dashboard.tsx` - Reference implementation
### Related Documentation
- `master-docs/API-COMPLETE-REFERENCE.md` - API contracts
- `REFACTOR_DOCS_INDEX.md` - Refactor documentation
- `.github/copilot-instructions.md` - AI agent guidelines
---
**Last Updated:** 2025-01-21
**Maintained By:** IGNY8 Development Team
**Status:** Living Document - Update when design patterns change

View File

@@ -0,0 +1,662 @@
# Sites Module UI Standardization Plan
**Goal:** Make Sites module UI consistent with IGNY8 global design standard (Planner/Writer/Dashboard)
**Reference:** See `IGNY8_DESIGN_STANDARD.md` for complete design patterns
**Status:** Ready for implementation
**Priority:** Medium-High (improves UX, accessibility, maintainability)
**Estimated Effort:** 4-6 hours
---
## Executive Summary
### Current State
The Sites module uses inconsistent UI patterns:
- ✅ Uses Button component correctly in many places
- ✅ Uses EnhancedMetricCard for metrics
- ❌ Uses `<button onClick={() => navigate(...)}` instead of `<Link to={...}>` in Quick Actions
- ❌ Missing ComponentCard wrapper for section organization
- ⚠️ Heavy inline Tailwind styling instead of reusable components
- ⚠️ Manual section headings instead of ComponentCard title prop
### Target State
Standardize Sites module to match Planner/Writer patterns:
- ✅ All navigation uses `<Link>` component (accessibility + keyboard nav)
- ✅ All sections wrapped in ComponentCard
- ✅ Quick Actions follow standard gradient icon pattern
- ✅ Consistent Button component usage (no raw buttons)
- ✅ Reduced inline Tailwind duplication
### Impact
- **Accessibility:** Better keyboard navigation and screen reader support
- **Maintainability:** Easier to update shared styles globally
- **Consistency:** Users see familiar patterns across all modules
- **Code Quality:** Less duplication, clearer component boundaries
---
## Audit Results
### Files Analyzed
-`Dashboard.tsx` (357 lines) - Main site dashboard
-`List.tsx` (sites list and creation)
-`Settings.tsx` (site settings tabs)
-`Content.tsx` (content management)
-`PageManager.tsx` (page management)
-`PostEditor.tsx` (post editing)
-`SyncDashboard.tsx` (sync status)
-`DeploymentPanel.tsx` (deployment)
-`Preview.tsx` (site preview)
-`Manage.tsx` (site management)
-`Editor.tsx` (site editor)
### Issues Found
#### 1. Navigation Pattern Inconsistency
**Files affected:** Dashboard.tsx (5 instances)
**Current (❌):**
```tsx
<button
onClick={() => navigate(`/sites/${siteId}/pages`)}
className="flex items-center gap-4 p-6 rounded-xl border-2 border-slate-200 bg-white hover:border-[var(--color-primary)] hover:shadow-lg transition-all group"
>
{/* ... */}
</button>
```
**Target (✅):**
```tsx
<Link
to={`/sites/${siteId}/pages`}
className="flex items-center gap-4 p-6 rounded-xl border-2 border-slate-200 bg-white hover:border-[var(--color-primary)] hover:shadow-lg transition-all group"
>
{/* ... */}
</Link>
```
**Reason:** Link provides better accessibility, keyboard navigation, and browser features (right-click open in new tab)
---
#### 2. Missing ComponentCard Wrapper
**Files affected:** Dashboard.tsx, List.tsx, Settings.tsx
**Current (❌):**
```tsx
<div className="mb-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Quick Actions
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{/* actions */}
</div>
</div>
```
**Target (✅):**
```tsx
<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">
{/* actions */}
</div>
</ComponentCard>
```
**Reason:** Consistent section styling, automatic dark mode support, less boilerplate
---
#### 3. Button + Navigate Anti-Pattern
**Files affected:** Content.tsx (2 instances), Editor.tsx (2 instances), List.tsx (1 instance)
**Current (❌):**
```tsx
<Button onClick={() => navigate('/sites/builder')} variant="primary">
Create New Site
</Button>
```
**Target (✅):**
```tsx
<Button as={Link} to="/sites/builder" variant="primary">
Create New Site
</Button>
```
**Reason:** Button component supports `as` prop for Link rendering while maintaining Button styles
---
#### 4. Inline Tailwind Duplication
**Files affected:** Settings.tsx (30+ instances), Content.tsx, List.tsx
**Example:**
```tsx
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
```
**Solution:** Extract to reusable Input component or use existing form components
---
#### 5. Raw Button Elements
**Files affected:** Multiple (50+ instances found)
**Pattern:** Some `<button>` tags for non-navigation actions (modals, toggles) - these are acceptable if not for navigation
---
## Refactoring Plan by File
### Phase 1: Dashboard.tsx (High Priority)
**Lines affected:** 254-324 (Quick Actions section)
**Changes:**
1. Import Link from react-router-dom
2. Replace 5 `<button onClick={navigate}>` with `<Link to>`
3. Wrap Quick Actions section in ComponentCard
4. Remove manual heading, use ComponentCard title prop
**Before:**
```tsx
<div className="mb-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Quick Actions
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<button onClick={() => navigate(`/sites/${siteId}/pages`)} className="...">
{/* ... */}
</button>
</div>
</div>
```
**After:**
```tsx
<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">
<Link to={`/sites/${siteId}/pages`} className="...">
{/* ... */}
</Link>
</div>
</ComponentCard>
```
**Estimated Time:** 30 minutes
---
### Phase 2: Content.tsx (Medium Priority)
**Lines affected:** 133, 214 (Button with navigate)
**Changes:**
1. Replace `<Button onClick={() => navigate(...)}>` with `<Button as={Link} to={...}>`
2. Verify variant prop consistency
3. Test navigation after change
**Before:**
```tsx
<Button onClick={() => navigate(`/sites/${siteId}/posts/new`)} variant="primary">
Create New Post
</Button>
```
**After:**
```tsx
<Button as={Link} to={`/sites/${siteId}/posts/new`} variant="primary">
Create New Post
</Button>
```
**Estimated Time:** 15 minutes
---
### Phase 3: List.tsx (Medium Priority)
**Lines affected:** 670 (Button with navigate), filter/tab sections
**Changes:**
1. Replace `<Button onClick={() => navigate('/sites/builder')}` with `<Button as={Link} to="/sites/builder">`
2. Consider extracting filter section to reusable component
3. Review tab navigation pattern for consistency
**Estimated Time:** 30 minutes
---
### Phase 4: Settings.tsx (Low Priority - Large File)
**Lines affected:** 30+ inline Tailwind instances
**Changes:**
1. Extract repeated input styling to shared component
2. Consider creating SettingsSection component (like ComponentCard but for tabs)
3. Review tab navigation pattern
4. Consolidate status badge styling
**Deferred:** This file needs deeper refactor - consider separate task
**Estimated Time:** 2 hours (deferred)
---
### Phase 5: Editor.tsx (Low Priority)
**Lines affected:** 117, 147 (Button with navigate)
**Changes:**
1. Replace Button + navigate with Button as Link
2. Verify navigation flow
**Estimated Time:** 10 minutes
---
### Phase 6: Other Files
**Files:** SyncDashboard.tsx, PageManager.tsx, DeploymentPanel.tsx, Preview.tsx, Manage.tsx
**Review:** Most raw buttons here are for actions (not navigation) - acceptable usage
**Action:** Verify each instance is truly an action (modal, toggle) and not navigation
**Estimated Time:** 30 minutes audit
---
## Implementation Steps (Detailed)
### Step 1: Create Quick Action Component (Optional - DRY)
**File:** `frontend/src/components/sites/SiteActionCard.tsx`
**Purpose:** Extract repeated Quick Action card pattern
**Props:**
```tsx
interface SiteActionCardProps {
to: string;
icon: ReactNode;
title: string;
description: string;
gradientColor: 'primary' | 'success' | 'warning' | 'purple';
hoverColor: string;
}
```
**Benefits:**
- Reduce duplication (5 cards in Dashboard become 5 component calls)
- Consistent styling automatically
- Easier to update globally
**Decision:** Create if time permits - not critical path
---
### Step 2: Dashboard.tsx Refactor
**File:** `frontend/src/pages/Sites/Dashboard.tsx`
**Line-by-line changes:**
1. **Add import:**
```tsx
import { Link } from 'react-router-dom';
import ComponentCard from '../../components/common/ComponentCard';
```
2. **Replace Quick Actions section (lines 245-324):**
**OLD:**
```tsx
{/* 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>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<button
onClick={() => navigate(`/sites/${siteId}/pages`)}
className="flex items-center gap-4 p-6 rounded-xl border-2 border-slate-200 bg-white hover:border-[var(--color-primary)] hover:shadow-lg transition-all group"
>
```
**NEW:**
```tsx
{/* 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">
<Link
to={`/sites/${siteId}/pages`}
className="flex items-center gap-4 p-6 rounded-xl border-2 border-slate-200 bg-white hover:border-[var(--color-primary)] hover:shadow-lg transition-all group"
>
```
3. **Repeat for all 5 action cards:**
- Manage Pages → `/sites/${siteId}/pages`
- Manage Content → `/sites/${siteId}/content`
- Integrations → `/sites/${siteId}/settings?tab=integrations`
- Sync Dashboard → `/sites/${siteId}/sync`
- Deploy Site → `/sites/${siteId}/deploy`
4. **Update Recent Activity section (optional):**
```tsx
<ComponentCard title="Recent Activity">
{/* content */}
</ComponentCard>
```
---
### Step 3: Content.tsx Refactor
**File:** `frontend/src/pages/Sites/Content.tsx`
**Changes:**
1. **Import Link:**
```tsx
import { Link } from 'react-router-dom';
```
2. **Replace line 133:**
```tsx
// OLD
<Button onClick={() => navigate(`/sites/${siteId}/posts/new`)} variant="primary">
// NEW
<Button as={Link} to={`/sites/${siteId}/posts/new`} variant="primary">
```
3. **Replace line 214 (duplicate):**
```tsx
// OLD
<Button onClick={() => navigate(`/sites/${siteId}/posts/new`)} variant="primary">
// NEW
<Button as={Link} to={`/sites/${siteId}/posts/new`} variant="primary">
```
---
### Step 4: List.tsx Refactor
**File:** `frontend/src/pages/Sites/List.tsx`
**Changes:**
1. **Import Link:**
```tsx
import { Link } from 'react-router-dom';
```
2. **Replace line 670:**
```tsx
// OLD
<Button onClick={() => navigate('/sites/builder')} variant="outline">
// NEW
<Button as={Link} to="/sites/builder" variant="outline">
```
3. **Review filter buttons (lines 681-693):**
These appear to be actual buttons (state toggles), not navigation - keep as-is
---
### Step 5: Editor.tsx Refactor
**File:** `frontend/src/pages/Sites/Editor.tsx`
**Changes:**
1. **Import Link:**
```tsx
import { Link } from 'react-router-dom';
```
2. **Replace line 117:**
```tsx
// OLD
<Button onClick={() => navigate('/sites/builder')} variant="primary">
// NEW
<Button as={Link} to="/sites/builder" variant="primary">
```
3. **Replace line 147:**
```tsx
// OLD
<Button onClick={() => navigate('/sites/builder')} variant="primary">
// NEW
<Button as={Link} to="/sites/builder" variant="primary">
```
---
### Step 6: Final Audit
**Files:** All Sites pages
**Checklist:**
- [ ] No `<button onClick={() => navigate(...)}` for navigation
- [ ] All navigation uses Link or Button with `as={Link}`
- [ ] Section headers use ComponentCard where appropriate
- [ ] EnhancedMetricCard used for all metrics
- [ ] Button component variants consistent
- [ ] No raw `<button>` for navigation (actions are OK)
---
## Testing Plan
### Visual Regression Testing
1. Compare Sites Dashboard to Planner Dashboard side-by-side
2. Verify Quick Actions grid layout matches (1/2/3 columns)
3. Verify gradient icon boxes match size and colors
4. Verify hover states match (border color, shadow)
5. Verify ComponentCard styling matches other modules
### Functional Testing
1. Test keyboard navigation (Tab through actions, Enter to navigate)
2. Test right-click "Open in new tab" on all action cards
3. Test screen reader labels (use browser inspector)
4. Test all navigation paths work correctly
5. Test dark mode consistency
### Accessibility Testing
1. Run Lighthouse accessibility audit before/after
2. Verify all Links have proper href attributes (not onClick)
3. Verify focus indicators visible on keyboard nav
4. Verify semantic HTML (Links vs buttons)
### Code Quality Testing
1. Run TypeScript compiler - 0 errors
2. Run ESLint - 0 warnings on changed files
3. Verify no console errors in browser
4. Verify no duplicate imports
---
## Rollback Plan
### Pre-Implementation
1. Create feature branch: `feature/sites-ui-standardization`
2. Commit each file change separately for easy rollback
3. Test each file after change before moving to next
### If Issues Found
1. Revert specific file commit
2. Investigate issue in isolation
3. Re-apply fix and test
### Full Rollback
```bash
git checkout main -- frontend/src/pages/Sites/Dashboard.tsx
git checkout main -- frontend/src/pages/Sites/Content.tsx
# etc.
```
---
## Success Criteria
### Must Have (P0)
- ✅ All Quick Actions use Link component (not button + navigate)
- ✅ Dashboard Quick Actions wrapped in ComponentCard
- ✅ Zero TypeScript errors
- ✅ All navigation paths work correctly
### Should Have (P1)
- ✅ Button + navigate replaced with Button as Link
- ✅ Keyboard navigation works on all action cards
- ✅ Visual consistency with Planner/Writer modules
### Nice to Have (P2)
- ⏸ SiteActionCard component extracted (DRY improvement)
- ⏸ Settings.tsx input styling standardized (larger refactor)
- ⏸ Status badge component extracted
---
## Risk Assessment
### Low Risk
- Dashboard.tsx Quick Actions refactor (structural change, low impact)
- Content/List/Editor Button changes (simple prop change)
### Medium Risk
- ComponentCard integration (may affect spacing/layout)
- Link component behavior differences (unlikely but possible)
### High Risk
- None identified
### Mitigation
- Test in dev environment before production
- Create feature flag if needed (not expected)
- Monitor error logs after deployment
---
## Timeline
### Immediate (Today)
- ✅ Design standard documentation (DONE)
- ✅ Audit sites pages (DONE)
- ✅ Create refactoring plan (DONE)
### Phase 1 (1-2 hours)
- Dashboard.tsx Quick Actions refactor
- Test visual consistency
- Test navigation functionality
### Phase 2 (30 minutes)
- Content.tsx, List.tsx, Editor.tsx refactors
- Test all navigation paths
### Phase 3 (30 minutes)
- Final audit and QA
- Accessibility testing
- Documentation update
### Total Estimated Time: 2-3 hours active work
---
## Documentation Updates
### After Implementation
1. Update DESIGN_SYSTEM.md with Sites module compliance
2. Add Sites Dashboard to reference implementations list
3. Document SiteActionCard component (if created)
4. Update CHANGELOG.md with UI standardization entry
---
## Future Enhancements
### Beyond This Refactor
1. Extract Settings.tsx tab pattern to reusable component
2. Create FormInput component for repeated input styling
3. Standardize status badge patterns across all modules
4. Add loading states to all navigation actions
5. Add transition animations to match Planner/Writer
### Technical Debt Reduction
1. Audit all inline Tailwind usage across Sites module
2. Create Sites-specific component library (like dashboard components)
3. Consolidate color usage (ensure CSS variables used consistently)
---
## Questions & Decisions
### Open Questions
- [ ] Should we create SiteActionCard component now or later?
- **Decision:** Later - keep first pass simple, extract after pattern proven
- [ ] Should Settings.tsx be included in this refactor?
- **Decision:** No - defer to separate task due to complexity
- [ ] Should we add analytics tracking to navigation events?
- **Decision:** Out of scope - separate feature request
### Decisions Made
- ✅ Use Link component (not button + navigate) for all navigation
- ✅ Use ComponentCard for section organization
- ✅ Use Button `as={Link}` pattern for button-styled navigation
- ✅ Defer Settings.tsx refactor to separate task
- ✅ Keep PageManager.tsx raw buttons (mostly actions, not navigation)
---
## Appendix: Code Snippets
### Standard Quick Action Card Pattern
```tsx
<Link
to={path}
className="flex items-center gap-4 p-6 rounded-xl border-2 border-slate-200 bg-white hover:border-[var(--color-primary)] hover:shadow-lg transition-all group"
>
<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">
<Icon className="h-6 w-6" />
</div>
<div className="flex-1 text-left">
<h4 className="font-semibold text-slate-900 mb-1">Action Title</h4>
<p className="text-sm text-slate-600">Action description</p>
</div>
<ArrowRightIcon className="h-5 w-5 text-slate-400 group-hover:text-[var(--color-primary)] transition" />
</Link>
```
### Gradient Color Reference
```tsx
// Primary (Blue)
from-[var(--color-primary)] to-[var(--color-primary-dark)]
hover:border-[var(--color-primary)]
group-hover:text-[var(--color-primary)]
// Success (Green)
from-[var(--color-success)] to-[var(--color-success-dark)]
hover:border-[var(--color-success)]
group-hover:text-[var(--color-success)]
// Warning (Orange)
from-[var(--color-warning)] to-[var(--color-warning-dark)]
hover:border-[var(--color-warning)]
group-hover:text-[var(--color-warning)]
// Purple
from-[var(--color-purple)] to-[var(--color-purple-dark)]
hover:border-[var(--color-purple)]
group-hover:text-[var(--color-purple)]
```
---
**End of Plan**
**Next Steps:**
1. Review this plan with team (if applicable)
2. Create feature branch
3. Start Phase 1 implementation (Dashboard.tsx)
4. Test and iterate
5. Deploy and monitor
**Prepared By:** GitHub Copilot
**Date:** 2025-01-21
**Version:** 1.0

View File

@@ -500,7 +500,7 @@ CORS_EXPOSE_HEADERS = [
JWT_SECRET_KEY = os.getenv('JWT_SECRET_KEY', SECRET_KEY)
JWT_ALGORITHM = 'HS256'
JWT_ACCESS_TOKEN_EXPIRY = timedelta(minutes=15)
JWT_REFRESH_TOKEN_EXPIRY = timedelta(days=7)
JWT_REFRESH_TOKEN_EXPIRY = timedelta(days=30) # Extended to 30 days for persistent login
# Celery Configuration
CELERY_BROKER_URL = os.getenv('CELERY_BROKER_URL', f"redis://{os.getenv('REDIS_HOST', 'redis')}:{os.getenv('REDIS_PORT', '6379')}/0")

View File

@@ -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"

View File

@@ -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>
);

View File

@@ -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>

View File

@@ -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>
);
}

View File

@@ -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...</>

View File

@@ -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>
);
},
);

View File

@@ -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) --> */}

View File

@@ -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);

View File

@@ -127,6 +127,7 @@ export default function AutomationRules() {
icon: <BoltIcon />,
color: 'purple',
}}
navigation={<ModuleNavigationTabs tabs={automationTabs} />}
/>
<div className="flex items-center justify-between">

View File

@@ -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">

View File

@@ -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">

View File

@@ -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

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}
>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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">

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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">

View File

@@ -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}

View File

@@ -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}

View File

@@ -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}

View File

@@ -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

View File

@@ -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);
}