ui frotneedn fixes
This commit is contained in:
225
BUTTON_STANDARDIZATION_COMPLETE.md
Normal file
225
BUTTON_STANDARDIZATION_COMPLETE.md
Normal 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
515
IGNY8_DESIGN_STANDARD.md
Normal 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
|
||||
662
SITES_MODULE_REFACTOR_PLAN.md
Normal file
662
SITES_MODULE_REFACTOR_PLAN.md
Normal 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
|
||||
@@ -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")
|
||||
|
||||
@@ -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