finalizing app adn fixes

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-25 22:58:21 +00:00
parent 4bffede052
commit 91525b8999
19 changed files with 2498 additions and 555 deletions

867
PRE-LAUNCH-AUDIT.md Normal file
View File

@@ -0,0 +1,867 @@
# Pre-Launch System Audit
**Date:** December 25, 2025
**Purpose:** Identify functional workflow gaps and improvements for pre-launch QA
**Scope:** Non-cosmetic, professional, workflow-based issues only
---
## Table of Contents
1. [Dashboard](#1-dashboard)
2. [SETUP Modules](#2-setup-modules)
3. [WORKFLOW Modules](#3-workflow-modules)
4. [ACCOUNT Modules](#4-account-modules)
5. [HELP Module](#5-help-module)
6. [Sidebar & Navigation](#6-sidebar--navigation)
7. [Summary & Prioritization](#7-summary--prioritization)
---
## 1. Dashboard
**Route:** `/`
**Files:** `pages/Dashboard/Home.tsx`, `components/dashboard/*`
### Current Functionality
- Workflow Progress: 6-step pipeline visualization (Sites → Keywords → Clusters → Ideas → Content → Published)
- Quick Actions: 5 navigation shortcuts
- Key Metrics: 4 cards (Keywords, Articles, Images, Completion %)
- Credit Usage: Monthly allowance and usage bar
- Workflow Modules Guide: 8 info cards explaining modules
- Onboarding: Site creation wizard for new users
### Critical Gaps
| Issue | Impact | Details |
|-------|--------|---------|
| **No aggregated API endpoint** | Performance | Makes 6+ sequential API calls with 120ms delays to avoid rate limiting |
| **Published content count incorrect** | Data accuracy | Cannot distinguish published vs draft content |
| **Usage Summary is hardcoded** | Misleading | Shows fake "547 credits / $0.34" data |
| **Recent Activity is hardcoded** | Misleading | Static mock activity that never updates |
| **No real-time updates** | Stale data | Only refreshes on manual action |
### Missing Professional Features
| Feature | Why Important |
|---------|---------------|
| **Needs Attention section** | Users don't know what requires action |
| **Recent content activity** | No real list of recently created/published content |
| **Error/warning alerts** | No indication of failed syncs, low credits, config issues |
| **Pipeline queue depth** | No visibility into items waiting at each stage |
| **Automation status** | No run status, last run time, or items processed |
| **Site health/sync status** | No WordPress sync health indicator |
### Workflow Issues
1. **Quick Actions don't adapt to user state** - Shows same 5 actions regardless of workflow stage
2. **Workflow Completion % is misleading** - Formula doesn't reflect real content-from-keywords ratio
3. **Modules Guide not dismissible** - 8 large cards always shown, no way to hide
4. **Chart widget code exists but unused** - Dead code, no trend visualization
### Recommendations
**Priority 1 - Must Fix:**
- [ ] Create `/v1/dashboard/summary/` aggregated endpoint
- [ ] Fix published content count (separate published vs draft)
- [ ] Replace hardcoded usage data with real billing data
- [ ] Remove or implement real recent activity
**Priority 2 - High Value:**
- [ ] Add "Needs Attention" section (pending reviews, failed syncs, low credits)
- [ ] Add actionable pending task count per pipeline stage
- [ ] Add automation run status display
- [ ] Add WordPress sync health indicator
**Priority 3 - UX Polish:**
- [ ] Make Quick Actions contextual based on workflow state
- [ ] Add loading skeleton
- [ ] Make Workflow Modules Guide dismissible
- [ ] Fix or remove trend chart code
---
## 2. SETUP Modules
### 2.1 Add Keywords
**Route:** `/setup/add-keywords`
**Files:** `pages/Setup/AddKeywords.tsx`
**Tabs:** None (single page)
#### Current Functionality
- Displays global seed keywords filtered by active site's industry/sector
- Browse pre-populated keyword database (admin-imported CSV)
- Add selected keywords to Planner workflow
- Tracks already-added keywords
- Bulk selection and bulk add
- Filters: Search, Country, Difficulty
- Admin CSV upload capability
#### Functional Gaps
| Issue | Impact |
|-------|--------|
| **Sector requirement unclear** | Buttons disabled with no tooltip explaining why |
| **No keyword research integration** | Can only browse pre-imported seeds, no external discovery |
| **No manual keyword entry** | Cannot add custom keywords not in seed database |
| **No keyword details/preview** | No SERP features, trends, or related keywords visible |
| **No "already added" filter** | Cannot filter to show only not-yet-added keywords |
#### Workflow Issues
- No "Next Step" CTA after adding keywords → users don't know to go to Planner
- No keyword count summary (X in workflow, Y available)
#### Recommendations
- [ ] Add "Next: Plan Your Content →" button after keywords added
- [ ] Add "Show not-yet-added only" filter
- [ ] Add manual keyword entry form
- [ ] Add tooltip explaining disabled state when no sector selected
- [ ] Add keyword count summary
---
### 2.2 Content Settings
**Route:** `/account/content-settings`
**Files:** `pages/account/ContentSettingsPage.tsx`
**Tabs:** Content Generation, Publishing, Image Settings
#### Current Functionality
- **Content Generation:** Append to prompt, default tone, default length
- **Publishing:** Auto-publish toggle, keep updated toggle
- **Image Settings:** Quality, style, sizes, format (DALL-E 2/3/Runware)
#### Critical Gaps
| Issue | Impact | Severity |
|-------|--------|----------|
| **Content Generation NOT PERSISTED** | Settings appear to save but don't - TODO comments in code | 🔴 Critical |
| **Publishing NOT PERSISTED** | Same issue - no backend API implemented | 🔴 Critical |
| **Only Image Settings work** | Only tab with actual API integration | 🔴 Critical |
| **No per-site settings** | Global only, but multi-site users need site-specific | 🟠 High |
#### Workflow Issues
- **False confidence**: Users see "Settings saved successfully" but nothing is saved
- No indication that Content Generation/Publishing tabs are non-functional
- Disconnected from Thinker prompts (which also affect content generation)
#### Recommendations
- [ ] 🔴 **CRITICAL:** Implement backend endpoints for Content Generation settings
- [ ] 🔴 **CRITICAL:** Implement backend endpoints for Publishing settings
- [ ] Either hide non-functional tabs or mark as "Coming Soon"
- [ ] Add relationship explanation to Thinker prompts
- [ ] Consider per-site overrides
---
### 2.3 Sites
**Route:** `/sites`
**Files:** `pages/Sites/List.tsx`, `pages/Sites/SiteSettings.tsx`, `pages/Sites/SiteDashboard.tsx`
**Tabs:** Site Settings has 3 tabs (General, Integrations, Content Types)
#### Current Functionality
- List all sites with filtering (search, type, hosting, status)
- Create sites via WorkflowGuide (requires industry + sectors)
- Activate/deactivate sites
- Navigate to site dashboard, content, settings
- Settings: Name, URL, SEO, WordPress integration, content type mapping
#### Functional Gaps
| Issue | Impact |
|-------|--------|
| **Dashboard stats are mock data** | `getSiteDashboardStats` returns hardcoded zeros |
| **No inline editing** | Must navigate to settings to change site name |
| **No site cloning** | Cannot duplicate site configuration |
| **Duplicate pages** | List.tsx and Manage.tsx overlap functionality |
| **No bulk operations** | Cannot bulk activate/delete sites |
#### Workflow Issues
- Complex site creation requires industry/sector upfront (users may not know yet)
- Integration tab default after creation confuses non-technical users
- No setup progress indicator showing what's configured vs pending
#### Recommendations
- [ ] Add site setup checklist/progress indicator on cards
- [ ] Allow site creation without immediate WordPress setup
- [ ] Add inline editing for site name from list
- [ ] Remove or merge Manage.tsx if redundant
- [ ] Implement real site statistics endpoint
- [ ] Add "Skip integration for now" option
---
### 2.4 Thinker (Admin Only)
**Route:** `/thinker/prompts`
**Files:** `pages/Thinker/Prompts.tsx`, `pages/Thinker/AuthorProfiles.tsx`, `pages/Thinker/Strategies.tsx`, `pages/Thinker/ImageTesting.tsx`
**Tabs:** Prompts, Author Profiles, Strategies (Coming Soon), Image Testing (Coming Soon)
#### Current Functionality
- **Prompts:** View/edit AI prompt templates (Clustering, Ideas, Content, Images, etc.)
- **Author Profiles:** CRUD for author profiles (name, description, tone, language)
- **Strategies:** Coming Soon placeholder
- **Image Testing:** Coming Soon placeholder
#### Functional Gaps
| Issue | Impact |
|-------|--------|
| **50% Coming Soon** | Strategies + Image Testing are placeholders | 🟠 High |
| **No prompt testing** | Cannot preview prompt output before saving |
| **No prompt versioning** | No history or rollback capability |
| **No per-site prompts** | All prompts global, no site-specific variations |
| **Author Profiles not connected** | Unclear how/where they're used in workflow |
| **No variable reference** | Placeholders like `[IGNY8_KEYWORDS]` undocumented |
#### Workflow Issues
- Admin-only with no explanation of relationship to Content Settings (user-accessible)
- No prompt categories in UI (hardcoded grouping)
- Strategies/Image Testing pages offer no value
#### Recommendations
- [ ] Either complete or hide Strategies and Image Testing
- [ ] Add prompt testing capability (preview with sample data)
- [ ] Add prompt version history
- [ ] Document relationship between Content Settings and Thinker Prompts
- [ ] Show where Author Profiles are used
- [ ] Add prompt variable reference documentation
---
### SETUP Cross-Module Issues
| Issue | Impact |
|-------|--------|
| **No guided flow** | After setup tasks, no clear path to start content workflow |
| **Scattered settings** | Content settings split across 3 locations |
| **No onboarding checklist** | No unified "Setup Complete" indicator |
#### Setup Completion Checklist Needed:
- [ ] Site created
- [ ] Industry/Sectors selected
- [ ] Integration configured (or skipped)
- [ ] Keywords added
- [ ] Content settings configured
---
## 3. WORKFLOW Modules
### 3.1 Planner
**Route:** `/planner/keywords`
**Files:** `pages/Planner/Keywords.tsx`, `pages/Planner/Clusters.tsx`, `pages/Planner/ClusterView.tsx`, `pages/Planner/Ideas.tsx`, `pages/Planner/KeywordOpportunities.tsx`
**Tabs:** Keywords, Clusters, Ideas (in-page navigation)
#### Current Functionality
- **Keywords:** CRUD, bulk status updates, auto-cluster AI, filters
- **Clusters:** CRUD, bulk operations, auto-generate ideas AI
- **Ideas:** CRUD, bulk queue to writer, filters
- **Flow:** Keywords → Auto-Cluster → Clusters → Auto-Generate Ideas → Ideas → Queue to Writer
#### Functional Gaps
| Issue | Impact | Severity |
|-------|--------|----------|
| **KeywordOpportunities hidden** | Page exists at `/planner/keyword-opportunities` but NOT in navigation tabs | 🟠 High |
| **No "Add to Existing Cluster"** | Auto-cluster creates new clusters only, can't add to existing | 🟡 Medium |
| **No cluster merge/split** | Cannot combine or split clusters | 🟡 Medium |
| **No cluster progress indicator** | Can't see which clusters already have ideas generated | 🟠 High |
| **Ideas missing queued count** | No indicator of how many are pending vs processed | 🟡 Medium |
#### Workflow Issues
- Seed keyword system (KeywordOpportunities) is orphaned from main workflow
- Cluster → Ideas transition unclear (which clusters need ideas?)
- No return path from Ideas to source cluster
#### Recommendations
- [ ] **Add KeywordOpportunities to Planner tabs** (Critical for onboarding)
- [ ] Add "Assign to Cluster" bulk action for existing clusters
- [ ] Add Ideas Count badge on Clusters table
- [ ] Make cluster name clickable on Ideas page
---
### 3.2 Writer
**Route:** `/writer/tasks`
**Files:** `pages/Writer/Tasks.tsx`, `pages/Writer/Drafts.tsx`, `pages/Writer/ContentView.tsx`, `pages/Writer/Images.tsx`, `pages/Writer/Review.tsx`, `pages/Writer/Published.tsx`
**Tabs:** Queue, Drafts, Images, Review, Published
#### Current Functionality
- **Tasks:** CRUD, generate content (row action only), generate images bulk
- **Drafts:** List drafts, view details, status updates
- **Images:** Grouped by content, image generation
- **Review:** Status=review filter, publish to WordPress
- **Published:** Status=published, WordPress sync status
#### Functional Gaps
| Issue | Impact | Severity |
|-------|--------|----------|
| **No bulk content generation** | Must click each row individually, `generate_content` removed from bulk | 🔴 Critical |
| **No content editing** | ContentView is read-only, no inline editing | 🔴 Critical |
| **No manual task creation** | Must go through Ideas, can't create task directly | 🟠 High |
| **No content regeneration** | Can't regenerate with different params, must delete & re-queue | 🟠 High |
| **Review → Published manual only** | No simple "Mark as Published" for non-WordPress | 🟡 Medium |
#### Workflow Issues
- Status progression confusion (Draft → Review → Published requires different pages)
- ContentView missing actions bar (must return to list for actions)
- Images detached from content workflow
- No `send_to_linker` action (only `send_to_optimizer`)
#### Recommendations
- [ ] 🔴 **CRITICAL:** Add bulk content generation with rate limiting
- [ ] 🔴 **CRITICAL:** Implement inline content editing in ContentView
- [ ] Add content action bar (Edit, Regenerate, Add Images, Optimize, Link, Publish)
- [ ] Add `send_to_linker` row action
- [ ] Add manual task creation capability
---
### 3.3 Automation
**Route:** `/automation`
**Files:** `pages/Automation/Dashboard.tsx`
**Tabs:** None (single page)
#### Current Functionality
- Pipeline overview (7 stages: Keywords→Clusters→Ideas→Tasks→Content→Image Prompts→Images)
- Schedule configuration (frequency, time, enable/disable)
- Run controls (Run Now, Pause, Resume)
- Real-time progress polling
- Metrics display and activity log
#### Functional Gaps
| Issue | Impact |
|-------|--------|
| **No stage-by-stage control** | All-or-nothing, can't run individual stages |
| **No review gate config** | Stage 7 "Review Gate" has no UI to configure rules |
| **No error recovery** | If stage fails, must rerun entire pipeline |
| **No batch size config** | Can't throttle items per run |
| **No dry run/preview** | Can't see what WOULD process before running |
| **Activity log not filterable** | Can't filter by stage, status, or date |
#### Workflow Issues
- Credit estimation unclear (labeled "content pieces" but shows credits?)
- Run history depth unknown, no pagination
- No indication of manual vs automated items
#### Recommendations
- [ ] Add stage toggles (enable/disable individual stages)
- [ ] Add preview mode (show items that will process)
- [ ] Add retry for failed items (per-item or per-stage)
- [ ] Add activity log filters
- [ ] Clarify credit vs content labeling
---
### 3.4 Linker
**Route:** `/linker/content`
**Files:** `pages/Linker/Content.tsx`, `pages/Linker/Dashboard.tsx` (not routed)
**Tabs:** Content only (Dashboard exists but hidden)
#### Current Functionality
- Content list with link count and version
- Single-item "Add Links" button
- Batch linking capability (code exists, UI unclear)
- Recent results display (last 3)
#### Functional Gaps
| Issue | Impact |
|-------|--------|
| **Dashboard not exposed** | Dashboard.tsx exists but not in navigation |
| **No content filtering** | Can't filter by status, cluster, or link count |
| **No bulk selection UI** | `handleBatchLink` exists but no checkboxes |
| **No link preview/management** | Shows count but can't view/edit actual links |
| **No "Needs Linking" filter** | Can't find content with 0 links easily |
#### Workflow Issues
- Completely separate from Writer, requires manual navigation
- Link results only show "last 3" in session, no persistent history
- No cluster-based linking
#### Recommendations
- [ ] Add content filters (status, cluster, "needs linking")
- [ ] Add bulk selection checkboxes
- [ ] Add link details modal (show/manage individual links)
- [ ] Add "Link All New Content" action
- [ ] Integrate into Writer ContentView
---
### 3.5 Optimizer
**Route:** `/optimizer/content`
**Files:** `pages/Optimizer/Content.tsx`, `pages/Optimizer/AnalysisPreview.tsx` (orphaned), `pages/Optimizer/Dashboard.tsx` (not routed)
**Tabs:** Content only (Dashboard exists but hidden)
#### Current Functionality
- Content list with optimization scores
- Entry point selection (auto, writer, wordpress, external, manual)
- Single-item and batch optimize
- Score display (overall, SEO, readability, engagement)
#### Functional Gaps
| Issue | Impact |
|-------|--------|
| **Dashboard not exposed** | Dashboard.tsx exists but not in navigation |
| **AnalysisPreview orphaned** | Route exists but no UI link to it |
| **Limited filtering** | Only source/search, no score range filter |
| **No optimization history** | No before/after comparison |
| **No optimization settings** | Can't configure what aspects to optimize |
| **No suggested actions** | Scores show but no recommendations |
#### Workflow Issues
- Must navigate to Optimizer separately (disconnected from Writer)
- Analysis vs Optimize confusion (analyze never used)
- No re-optimization control
#### Recommendations
- [ ] Add Analysis Preview link ("Preview Scores" action)
- [ ] Add score-based filters ("Score < 70", "Needs SEO")
- [ ] Add optimization recommendations
- [ ] Add "Optimize All Below Threshold" bulk action
- [ ] Integrate into Writer (auto-analyze during draft)
---
### WORKFLOW Cross-Module Issues
| Issue | Impact |
|-------|--------|
| **No Planner → Writer visibility** | After queuing, must manually switch modules |
| **No Writer → Linker integration** | No "Add Links" button in content view |
| **No Writer → Optimizer integration** | Optimize exists but no score preview |
| **No cross-module notifications** | User doesn't know when AI tasks complete |
| **No breadcrumb navigation** | Can't see full workflow path (Cluster → Idea → Task → Content) |
#### Cross-Module Recommendations
- [ ] Add global notification system for completed tasks
- [ ] Add breadcrumb navigation showing workflow path
- [ ] Add "Next Step" suggestions after each action
- [ ] Unify content detail view with ALL actions available
- [ ] Add workflow progress indicator
---
## 4. ACCOUNT Modules
### 4.1 Account Settings
**Route:** `/account/settings`
**Files:** `pages/account/AccountSettingsPage.tsx`
**Tabs:** Account, Profile, Team
#### Current Functionality
- **Account Tab:** Organization name, billing email, full billing address, tax ID/VAT
- **Profile Tab:** First/last name, email, phone, timezone, language, notifications, security
- **Team Tab:** List team members, invite via email, remove members, role display
#### Functional Gaps
| Issue | Impact | Severity |
|-------|--------|----------|
| **Profile NOT connected to API** | Form saves nowhere - fake save with timeout | 🔴 Critical |
| **No role assignment on invite** | Only email/name collected, no role dropdown | 🟠 High |
| **No role editing for members** | Cannot change Member to Admin or vice versa | 🟠 High |
| **Change Password does nothing** | Static button with no functionality | 🔴 Critical |
| **No email verification** | Can change email without verification | 🟠 High |
| **No 2FA option** | Security section minimal | 🟡 Medium |
| **No account deletion** | Cannot close account | 🟡 Medium |
| **No session management** | Cannot view/revoke active sessions | 🟡 Medium |
#### Workflow Issues
- Orphaned `TeamManagement.tsx` file exists (395 lines, not routed)
- Inconsistent role system (shows Admin/Member but backend returns `is_admin` boolean)
- No pending invitation status or resend capability
- No team member limit enforcement display
#### Recommendations
- [ ] 🔴 **CRITICAL:** Implement profile API and connect Profile tab
- [ ] 🔴 **CRITICAL:** Implement password change functionality
- [ ] Add role selection to team invitation
- [ ] Add invitation management (resend, cancel pending)
- [ ] Show team member count vs plan limit
- [ ] Delete orphaned `TeamManagement.tsx`
---
### 4.2 Plans & Billing
**Route:** `/account/plans`
**Files:** `pages/account/PlansAndBillingPage.tsx`, `pages/Billing/CreditPurchase.tsx`
**Tabs:** Current Plan, Upgrade Plan, History
#### Current Functionality
- **Current Plan:** Plan name, status, credits, balance, renewal date, features
- **Upgrade:** Pricing table, plan comparison, change policy
- **History:** Invoices with PDF download, payments, payment methods
#### Functional Gaps
| Issue | Impact |
|-------|--------|
| **No proration preview** | Doesn't show prorated amount before upgrade |
| **Credit purchase not linked** | `/billing/credits` exists separately but not linked |
| **Cancellation is immediate** | No reason collection, no retention offers |
| **No payment failure retry** | If payment fails, no retry UI |
| **No downgrade proration display** | Policy exists but no calculation shown |
#### Workflow Issues
- Throttling errors surface directly to users ("Request was throttled. Retrying...")
- Cancel flow has no confirmation dialog
- Payment method supports bank_transfer, manual, stripe, paypal but UI only shows some
- No billing cycle visualization (renewal date not prominent)
#### Recommendations
- [ ] Add proration preview before plan changes
- [ ] Add confirmation dialog for cancellation
- [ ] Link credit purchase from this page
- [ ] Add cancellation reason collection
- [ ] Clean up throttling messages (use spinner)
---
### 4.3 Usage
**Route:** `/account/usage`
**Files:** `pages/account/UsageAnalyticsPage.tsx`, `pages/account/UsageLimits.tsx`, `pages/account/CreditActivity.tsx`
**Tabs:** Your Limits & Usage, Credit History, API Activity
#### Current Functionality
- **Quick Stats:** Credits left, used this month, monthly limit, usage %
- **Limits:** Hard limits (sites, users, keywords, clusters) + Monthly limits
- **Credit History:** Transaction log with type, amount, description
- **API Activity:** Call statistics, endpoint breakdown
#### Critical Gaps
| Issue | Impact | Severity |
|-------|--------|----------|
| **API Activity is HARDCODED** | Shows fake static values (1,234, 567, 342) not real data | 🔴 Critical |
| **Success rate is fake** | Hardcoded 98.5% | 🔴 Critical |
| **No usage alerts** | No notification when approaching limits | 🟠 High |
| **No per-site usage** | Can't see which site consumed what | 🟠 High |
| **No per-user usage** | Can't see team member individual usage | 🟠 High |
| **No usage export** | Cannot download usage report | 🟡 Medium |
| **No usage forecasting** | No "you'll run out in X days" | 🟡 Medium |
#### Workflow Issues
- No actionable insights (doesn't suggest upgrade when hitting limits)
- Credit history lacks context (no link to what was generated)
- Disconnected from billing (separate page to upgrade)
#### Recommendations
- [ ] 🔴 **CRITICAL:** Implement actual API activity tracking or hide tab
- [ ] Add usage alerts configuration (email at 80%, 90%, 100%)
- [ ] Add per-site/per-user usage breakdown
- [ ] Add "Upgrade" CTA when limits approaching
- [ ] Add usage export functionality
---
### 4.4 AI Models (Admin Only)
**Route:** `/settings/integration`
**Files:** `pages/Settings/IntegrationPage.tsx`
**Sections:** OpenAI Integration, Runware Integration, Image Generation, Site Integrations
#### Current Functionality
- **OpenAI:** Enable/disable, model selection, connection testing, validation
- **Runware:** Enable/disable, model selection, connection testing
- **Image Generation:** Service selection, model selection, image settings
- **Testing:** Generate test images with preview
- **Site Integrations:** Connected site management
#### Functional Gaps
| Issue | Impact | Severity |
|-------|--------|----------|
| **Fictional GPT model names** | Shows GPT-5.1, GPT-5.2 which don't exist | 🔴 Critical |
| **No fallback configuration** | If OpenAI fails, no automatic Runware fallback | 🟠 High |
| **No cost tracking** | Can't see integration costs | 🟡 Medium |
| **No integration health history** | Only current status, no uptime history | 🟡 Medium |
| **No API key rotation** | Can't rotate keys without disabling | 🟡 Medium |
| **No audit log** | No record of when settings changed | 🟠 High |
#### Workflow Issues
- Admin-only but affects all users (no scope clarification)
- Page mixes LLM models, image generation, AND site integrations
- Complex modal nesting (settings, details, form all separate)
- No preview of cost impact when changing models
#### Recommendations
- [ ] 🔴 **CRITICAL:** Fix GPT model names to actual models (gpt-4-turbo, gpt-4, gpt-3.5-turbo)
- [ ] Add integration change audit logging
- [ ] Add cost estimation when changing models
- [ ] Consider separating site integrations to own page
- [ ] Add fallback configuration
---
### ACCOUNT Cross-Module Issues
| Issue | Impact |
|-------|--------|
| **Multiple credit balance sources** | Plans, Usage, billingStore all fetch independently |
| **Fragmented billing pages** | PlansAndBillingPage, CreditPurchase, legacy routes |
| **Legacy routes still exist** | `/billing/overview`, `/team`, `/profile` all redirect |
| **No audit log across modules** | No record of who changed what when |
| **No notification preferences** | Cannot configure billing/usage email alerts |
---
## 5. HELP Module
### 5.1 Help & Docs
**Route:** `/help`
**Files:** `pages/Help/HelpCenter.tsx`, `pages/Help/Documentation.tsx` (placeholder), `pages/Help/SystemTesting.tsx` (placeholder), `pages/Help/FunctionTesting.tsx` (placeholder)
**Routes:** `/help`, `/help/docs`, `/help/system-testing`, `/help/function-testing`
#### Current Functionality
- Table of Contents with jump-to-section navigation
- Getting Started: Quick Start Guide, Workflow Overview
- Planner Module: Keywords, Clusters, Ideas documentation
- Writer Module: Tasks, Content, Images documentation
- Automation Setup overview
- FAQ section (~20 questions)
- Support CTA buttons (non-functional)
#### Critical Gaps
| Issue | Impact | Severity |
|-------|--------|----------|
| **Support dropdown link broken** | Goes to `/profile` which has NO route - 404 | 🔴 Critical |
| **Contact Support button does nothing** | `<button>` with no onClick handler | 🔴 Critical |
| **Feature Request button does nothing** | Same - no functionality | 🔴 Critical |
| **3 of 4 help pages empty** | `/help/docs`, `/help/system-testing`, `/help/function-testing` are placeholders | 🟠 High |
| **No actual support channel** | No email, chat, or ticket system | 🔴 Critical |
#### Missing Documentation
| Module | Status | Coverage |
|--------|--------|----------|
| Thinker | Active | 1 FAQ only - no section |
| Linker | Optional | Not mentioned |
| Optimizer | Optional | Not mentioned |
| Publisher | Active | Not documented |
| Sites/Site Builder | Active | Not documented |
| Billing/Credits | Active | 1 FAQ only |
| Account Settings | Active | Not documented |
#### Workflow Issues
- **No search functionality** - Must scroll or use TOC
- **No contextual help** - No in-app tooltips or "?" icons
- **No help sub-navigation** - Routes exist but no tabs visible
- **Stale content risk** - Hardcoded in TSX, requires deployment to update
#### Recommendations
- [ ] 🔴 **CRITICAL:** Fix Support dropdown link (`/profile``/help` or support page)
- [ ] 🔴 **CRITICAL:** Implement Contact Support (mailto: or external support URL)
- [ ] 🔴 **CRITICAL:** Add WordPress setup guide
- [ ] Document missing modules (Sites, Publisher, Thinker)
- [ ] Create Troubleshooting guide
- [ ] Add search functionality
- [ ] Add credit cost documentation
- [ ] Remove or implement placeholder pages
---
## 6. Sidebar & Navigation
**File:** `layout/AppSidebar.tsx`
### Current Structure
```
Dashboard (standalone)
├─ SETUP
│ ├─ Add Keywords → /setup/add-keywords
│ ├─ Content Settings → /account/content-settings
│ ├─ Sites (if site_builder enabled) → /sites
│ └─ Thinker (admin only, if thinker enabled) → /thinker/prompts
├─ WORKFLOW
│ ├─ Planner (if planner enabled) → /planner/keywords
│ ├─ Writer (if writer enabled) → /writer/tasks
│ ├─ Automation (if automation enabled) → /automation
│ ├─ Linker (if linker enabled) → /linker/content
│ └─ Optimizer (if optimizer enabled) → /optimizer/content
├─ ACCOUNT
│ ├─ Account Settings → /account/settings
│ ├─ Plans & Billing → /account/plans
│ ├─ Usage → /account/usage
│ └─ AI Models (admin only) → /settings/integration
└─ HELP
└─ Help & Docs → /help
```
### Navigation Gaps
| Issue | Impact |
|-------|--------|
| **KeywordOpportunities not in navigation** | `/planner/keyword-opportunities` exists but not accessible |
| **Linker Dashboard not exposed** | `/linker/dashboard` exists but only `/linker/content` in sidebar |
| **Optimizer Dashboard not exposed** | `/optimizer/dashboard` exists but only `/optimizer/content` in sidebar |
| **Help sub-pages hidden** | `/help/docs`, `/help/system-testing`, `/help/function-testing` not navigable |
| **Credit purchase not in sidebar** | `/billing/credits` exists but not accessible from sidebar |
### Menu Order Issues
| Current Order | Recommended Order | Reason |
|---------------|-------------------|--------|
| Add Keywords first in SETUP | Sites first | User should create site before adding keywords |
| Content Settings before Sites | Content Settings last | Configure after site is set up |
| Planner before Writer | Planner before Writer ✓ | Correct - follows workflow |
### Missing Navigation Features
| Feature | Impact |
|---------|--------|
| **No breadcrumb navigation** | User can't see full path (Cluster → Idea → Task → Content) |
| **No "Next Step" guidance** | After completing action, user doesn't know where to go |
| **No active section highlighting** | Sidebar doesn't show which section is active |
| **No keyboard navigation** | Can't navigate sidebar with keyboard |
| **No recent pages** | Can't quickly return to recently visited pages |
### Recommendations
- [ ] Add KeywordOpportunities to Planner sub-navigation
- [ ] Consider reordering SETUP: Sites → Add Keywords → Content Settings → Thinker
- [ ] Add breadcrumb navigation component
- [ ] Add "What's Next?" suggestions after key actions
- [ ] Consider exposing Dashboard pages for Linker/Optimizer
---
## 7. Summary & Prioritization
### 🔴 CRITICAL - Must Fix Before Launch
| # | Issue | Module | Type |
|---|-------|--------|------|
| 1 | **Content Generation/Publishing settings NOT SAVED** | Content Settings | Data Loss |
| 2 | **Profile tab NOT connected to API** | Account Settings | Data Loss |
| 3 | **Password change does nothing** | Account Settings | Security |
| 4 | **API Activity data is HARDCODED fake** | Usage | Misleading Data |
| 5 | **Support buttons do nothing** | Help | No Support Channel |
| 6 | **Support dropdown goes to 404** | Help | Broken Link |
| 7 | **Fictional GPT model names (GPT-5.1, 5.2)** | AI Models | Data Integrity |
| 8 | **No bulk content generation** | Writer | Core Workflow Blocked |
| 9 | **No content editing capability** | Writer | Core Workflow Blocked |
| 10 | **Dashboard has hardcoded usage/activity data** | Dashboard | Misleading Data |
### 🟠 HIGH - Significant Impact on User Experience
| # | Issue | Module |
|---|-------|--------|
| 11 | No role assignment/editing for team members | Account Settings |
| 12 | No proration preview for plan changes | Plans & Billing |
| 13 | No cancellation confirmation dialog | Plans & Billing |
| 14 | KeywordOpportunities hidden from navigation | Planner |
| 15 | No cluster progress indicator (which have ideas) | Planner |
| 16 | No content regeneration capability | Writer |
| 17 | 3 of 4 help pages are empty placeholders | Help |
| 18 | No dashboard API endpoint (6+ sequential calls) | Dashboard |
| 19 | Published content count incorrect | Dashboard |
| 20 | No "Needs Attention" section | Dashboard |
| 21 | No usage alerts when approaching limits | Usage |
| 22 | Sites Dashboard shows mock/zero data | Sites |
| 23 | Thinker has 50% "Coming Soon" pages | Thinker |
| 24 | No integration audit logging | AI Models |
### 🟡 MEDIUM - Professional Polish Needed
| # | Issue | Module |
|---|-------|--------|
| 25 | No manual keyword entry | Add Keywords |
| 26 | No "Next Step" CTA after actions | Add Keywords, Planner |
| 27 | No pending invitation management | Account Settings |
| 28 | No 2FA option | Account Settings |
| 29 | No per-site/per-user usage breakdown | Usage |
| 30 | No search in Help | Help |
| 31 | Missing module documentation (Sites, Thinker, etc.) | Help |
| 32 | Automation has no stage-by-stage control | Automation |
| 33 | Linker has no content filtering | Linker |
| 34 | Optimizer has no score-based filtering | Optimizer |
| 35 | No cross-module notifications | All |
| 36 | No breadcrumb navigation | All |
| 37 | Quick Actions don't adapt to user state | Dashboard |
---
### Implementation Phases
#### Phase 1: Critical Fixes (Must Complete)
Focus: Data integrity, security, core functionality
- Fix Content Settings API (Content Gen + Publishing tabs)
- Fix Profile API connection
- Implement password change
- Remove/fix hardcoded API Activity data
- Implement support channel (mailto or external URL)
- Fix Support dropdown link
- Fix GPT model names
- Add bulk content generation
- Add inline content editing
#### Phase 2: Core Workflow Improvements
Focus: User workflow efficiency
- Add dashboard aggregated API endpoint
- Add "Needs Attention" widget
- Add KeywordOpportunities to navigation
- Add cluster progress indicators
- Add content regeneration
- Add team role management
- Add plan change proration preview
- Add cancellation confirmation
#### Phase 3: Professional Polish
Focus: Edge cases and advanced features
- Complete Help documentation
- Add usage alerts
- Add per-site usage breakdown
- Add stage controls to Automation
- Add filtering to Linker/Optimizer
- Add breadcrumb navigation
- Add cross-module notifications
---
### Quick Wins (< 1 hour each)
1. Fix Support dropdown link (`/profile``/help`)
2. Add mailto: to Contact Support button
3. Fix GPT model names (rename to actual models)
4. Add cancellation confirmation dialog
5. Remove/hide API Activity tab until implemented
6. Add KeywordOpportunities to Planner tabs
7. Fix profile save to show "Coming Soon" instead of fake save
---
### Files to Delete (Orphaned/Duplicate)
| File | Reason |
|------|--------|
| `pages/account/TeamManagement.tsx` | Orphaned, functionality in AccountSettingsPage |
| `pages/Sites/Manage.tsx` | Duplicate of List.tsx |
| `pages/Help/Documentation.tsx` | Empty placeholder |
| `pages/Help/SystemTesting.tsx` | Empty placeholder |
| `pages/Help/FunctionTesting.tsx` | Empty placeholder |
| `pages/Thinker/Strategies.tsx` | Empty "Coming Soon" |
| `pages/Thinker/ImageTesting.tsx` | Empty "Coming Soon" |
---
**Total Issues Identified:** 37
**Critical:** 10
**High:** 14
**Medium:** 13

156
docs/UX-GUIDELINES.md Normal file
View File

@@ -0,0 +1,156 @@
# IGNY8 UX Guidelines
**Last Updated:** December 25, 2025
---
## Design Principles
### 1. Concise Labels
**Navigation & Tabs:** Keep labels short (1-2 words max)
- ✅ Good: `Queue`, `Drafts`, `Images`, `Review`, `Published`
- ❌ Bad: `Ready to Write`, `Finished Drafts`, `Review Before Publishing`
**Section Headers:** Use simple, consistent terminology
- ✅ Good: `SETUP`, `WORKFLOW`, `ACCOUNT`, `SETTINGS`
- ❌ Bad: `GET STARTED`, `CREATE CONTENT`, `MANAGE ACCOUNT`, `CONFIGURATION`
### 2. Consistent Terminology
Use the same term throughout the system:
| Concept | Correct Term | Avoid |
|---------|--------------|-------|
| Content measurement | "Content pieces" | "Credits" |
| Sidebar modules | Module name only | Verbose descriptions |
| Page titles | Match tab name | Flowery language |
### 3. Page Titles
Page titles should be:
- Short and descriptive
- Match the sidebar navigation
- Consistent with tab labels
```
Dashboard (not "Your Content Creation Dashboard")
Keywords (not "Your Keywords")
Drafts (not "Your Articles" or "Finished Drafts")
```
### 4. Descriptions & Helper Text
- Keep descriptions **short** (under 10 words)
- Put longer explanations in tooltips or Help pages
- Dashboard cards: 3-5 word descriptions maximum
```tsx
// ✅ Good
<ComponentCard title="Workflow Progress" desc="Track your content pipeline">
// ❌ Bad
<ComponentCard title="Your Content Journey" desc="Track your content creation progress from ideas to published articles">
```
### 5. Workflow Pipeline Labels
For pipeline stages, use arrow notation:
-`Keywords → Clusters`
-`Organize Keywords`
---
## Navigation Structure
### Sidebar Sections
```
Dashboard (standalone)
SETUP
├── Add Keywords
├── Sites
└── Thinker
WORKFLOW
├── Planner
├── Writer
├── Automation
├── Linker
└── Optimizer
ACCOUNT
├── Account Settings
├── Team
├── Plans & Billing
└── Usage
SETTINGS
├── Profile
├── AI Models
├── Publishing
└── Import / Export
HELP
└── Help & Docs
```
### Module Tab Labels
**Planner:** `Keywords` | `Clusters` | `Ideas`
**Writer:** `Queue` | `Drafts` | `Images` | `Review` | `Published`
**Thinker:** `Prompts` | `Author Profiles` | `Strategies` | `Image Testing`
---
## When to Add Explanatory Text
### DO add explanations for:
- Help & Documentation pages
- First-time user onboarding flows
- Error messages and empty states
- Tooltips on hover
### DON'T add explanations to:
- Navigation labels
- Tab labels
- Page headers
- Card descriptions on dashboards
---
## User-Facing Terminology
### Content & Pricing
| Internal (Backend) | User-Facing (Frontend) |
|-------------------|------------------------|
| `credits` | "content pieces" |
| `credits_remaining` | "X remaining" |
| `plan_credits_per_month` | "monthly allowance" |
| Purchase Credits | Upgrade Plan |
| Credit Balance | Content Usage |
### Actions
| Internal/Old | User-Facing |
|--------------|-------------|
| Generate | Create |
| Execute | Run |
| Configure | Set up |
| Insufficient credits | Content limit reached |
---
## Change History
| Date | Change |
|------|--------|
| Dec 25, 2025 | Reverted verbose navigation labels to concise terms |
| Dec 25, 2025 | Fixed Dashboard progress item descriptions |
| Dec 25, 2025 | Fixed Writer module tabs (Queue, Drafts, etc.) |
| Dec 25, 2025 | Fixed Planner module tabs (Keywords, Clusters, Ideas) |
| Dec 25, 2025 | Restored original Automation pipeline stage names |

View File

@@ -4,6 +4,7 @@ import { HelmetProvider } from "react-helmet-async";
import AppLayout from "./layout/AppLayout";
import { ScrollToTop } from "./components/common/ScrollToTop";
import ProtectedRoute from "./components/auth/ProtectedRoute";
import AdminRoute from "./components/auth/AdminRoute";
import GlobalErrorDisplay from "./components/common/GlobalErrorDisplay";
import LoadingStateMonitor from "./components/common/LoadingStateMonitor";
import { useAuthStore } from "./store/authStore";
@@ -64,8 +65,9 @@ const CreditsAndBilling = lazy(() => import("./pages/Settings/CreditsAndBilling"
const PurchaseCreditsPage = lazy(() => import("./pages/account/PurchaseCreditsPage"));
const PlansAndBillingPage = lazy(() => import("./pages/account/PlansAndBillingPage"));
const AccountSettingsPage = lazy(() => import("./pages/account/AccountSettingsPage"));
const TeamManagementPage = lazy(() => import("./pages/account/TeamManagementPage"));
// TeamManagementPage - Now integrated as tab in AccountSettingsPage
const UsageAnalyticsPage = lazy(() => import("./pages/account/UsageAnalyticsPage"));
const ContentSettingsPage = lazy(() => import("./pages/account/ContentSettingsPage"));
// Reference Data - Lazy loaded
const SeedKeywords = lazy(() => import("./pages/Reference/SeedKeywords"));
@@ -76,7 +78,7 @@ const IndustriesSectorsKeywords = lazy(() => import("./pages/Setup/IndustriesSec
// Settings - Lazy loaded
const GeneralSettings = lazy(() => import("./pages/Settings/General"));
const ProfileSettingsPage = lazy(() => import("./pages/settings/ProfileSettingsPage"));
// ProfileSettingsPage - Now integrated as tab in AccountSettingsPage
const Users = lazy(() => import("./pages/Settings/Users"));
const Subscriptions = lazy(() => import("./pages/Settings/Subscriptions"));
const SystemSettings = lazy(() => import("./pages/Settings/System"));
@@ -87,7 +89,7 @@ const Industries = lazy(() => import("./pages/Settings/Industries"));
const Integration = lazy(() => import("./pages/Settings/Integration"));
const Publishing = lazy(() => import("./pages/Settings/Publishing"));
const Sites = lazy(() => import("./pages/Settings/Sites"));
const ImportExport = lazy(() => import("./pages/Settings/ImportExport"));
// ImportExport - Removed from UI, individual pages have their own import/export
// Sites - Lazy loaded
const SiteList = lazy(() => import("./pages/Sites/List"));
@@ -176,13 +178,13 @@ export default function App() {
<Route path="/optimizer/content" element={<OptimizerContentSelector />} />
<Route path="/optimizer/analyze/:id" element={<AnalysisPreview />} />
{/* Thinker Module - Redirect dashboard to prompts */}
<Route path="/thinker" element={<Navigate to="/thinker/prompts" replace />} />
<Route path="/thinker/prompts" element={<Prompts />} />
<Route path="/thinker/author-profiles" element={<AuthorProfiles />} />
<Route path="/thinker/profile" element={<ThinkerProfile />} />
<Route path="/thinker/strategies" element={<Strategies />} />
<Route path="/thinker/image-testing" element={<ImageTesting />} />
{/* Thinker Module - Admin Only (Prompts & AI Configuration) */}
<Route path="/thinker" element={<AdminRoute><Navigate to="/thinker/prompts" replace /></AdminRoute>} />
<Route path="/thinker/prompts" element={<AdminRoute><Prompts /></AdminRoute>} />
<Route path="/thinker/author-profiles" element={<AdminRoute><AuthorProfiles /></AdminRoute>} />
<Route path="/thinker/profile" element={<AdminRoute><ThinkerProfile /></AdminRoute>} />
<Route path="/thinker/strategies" element={<AdminRoute><Strategies /></AdminRoute>} />
<Route path="/thinker/image-testing" element={<AdminRoute><ImageTesting /></AdminRoute>} />
{/* Billing Module */}
<Route path="/billing" element={<Navigate to="/billing/overview" replace />} />
@@ -195,8 +197,10 @@ export default function App() {
<Route path="/account/plans" element={<PlansAndBillingPage />} />
<Route path="/account/purchase-credits" element={<PurchaseCreditsPage />} />
<Route path="/account/settings" element={<AccountSettingsPage />} />
<Route path="/account/team" element={<TeamManagementPage />} />
{/* Legacy redirect - Team is now a tab in Account Settings */}
<Route path="/account/team" element={<Navigate to="/account/settings" replace />} />
<Route path="/account/usage" element={<UsageAnalyticsPage />} />
<Route path="/account/content-settings" element={<ContentSettingsPage />} />
{/* Reference Data */}
<Route path="/reference/seed-keywords" element={<SeedKeywords />} />
@@ -209,7 +213,8 @@ export default function App() {
<Route path="/setup/industries-sectors-keywords" element={<Navigate to="/setup/add-keywords" replace />} />
{/* Settings */}
<Route path="/settings/profile" element={<ProfileSettingsPage />} />
{/* Legacy redirect - Profile is now a tab in Account Settings */}
<Route path="/settings/profile" element={<Navigate to="/account/settings" replace />} />
<Route path="/settings" element={<GeneralSettings />} />
<Route path="/settings/users" element={<Users />} />
<Route path="/settings/subscriptions" element={<Subscriptions />} />
@@ -218,10 +223,12 @@ export default function App() {
<Route path="/settings/ai" element={<AISettings />} />
<Route path="/settings/plans" element={<Plans />} />
<Route path="/settings/industries" element={<Industries />} />
<Route path="/settings/integration" element={<Integration />} />
{/* AI Models Settings - Admin Only */}
<Route path="/settings/integration" element={<AdminRoute><Integration /></AdminRoute>} />
<Route path="/settings/publishing" element={<Publishing />} />
<Route path="/settings/sites" element={<Sites />} />
<Route path="/settings/import-export" element={<ImportExport />} />
{/* Legacy redirect - Import/Export removed, redirect to dashboard */}
<Route path="/settings/import-export" element={<Navigate to="/" replace />} />
{/* Sites Management */}
<Route path="/sites" element={<SiteList />} />

View File

@@ -0,0 +1,32 @@
import { ReactNode } from "react";
import { Navigate, useLocation } from "react-router-dom";
import { useAuthStore } from "../../store/authStore";
interface AdminRouteProps {
children: ReactNode;
}
/**
* AdminRoute component - guards routes requiring admin or staff privileges
* Redirects to dashboard if user is not admin/staff
*/
export default function AdminRoute({ children }: AdminRouteProps) {
const { user, isAuthenticated } = useAuthStore();
const location = useLocation();
// If not authenticated, ProtectedRoute will handle redirect
if (!isAuthenticated) {
return null;
}
// Check if user is admin or staff
const isAdmin = user?.role === 'admin' || user?.is_staff === true;
if (!isAdmin) {
// Redirect non-admin users to dashboard
console.log('AdminRoute: User is not admin/staff, redirecting to dashboard');
return <Navigate to="/" state={{ from: location }} replace />;
}
return <>{children}</>;
}

View File

@@ -13,6 +13,7 @@ import { WorkflowInsights, WorkflowInsight } from './WorkflowInsights';
interface PageHeaderProps {
title: string;
description?: string; // Optional page description shown below title
lastUpdated?: Date;
showRefresh?: boolean;
onRefresh?: () => void;
@@ -28,6 +29,7 @@ interface PageHeaderProps {
export default function PageHeader({
title,
description,
lastUpdated,
showRefresh = false,
onRefresh,
@@ -116,6 +118,9 @@ export default function PageHeader({
)}
<h2 className="text-2xl font-bold text-gray-800 dark:text-white/90">{title}</h2>
</div>
{description && (
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1 max-w-xl">{description}</p>
)}
{!hideSiteSector && (
<div className="flex items-center gap-3 mt-1">
{lastUpdated && (

View File

@@ -19,8 +19,6 @@ import {
UserCircleIcon,
} from "../icons";
import { useSidebar } from "../context/SidebarContext";
import SidebarWidget from "./SidebarWidget";
import { APP_VERSION } from "../config/version";
import { useAuthStore } from "../store/authStore";
import { useSettingsStore } from "../store/settingsStore";
import { useModuleStore } from "../store/moduleStore";
@@ -66,26 +64,32 @@ const AppSidebar: React.FC = () => {
const setupItems: NavItem[] = [
{
icon: <DocsIcon />,
name: "Find Keywords",
name: "Add Keywords",
path: "/setup/add-keywords",
},
{
icon: <PageIcon />,
name: "Content Settings",
path: "/account/content-settings",
},
];
// Add Sites (Site Builder) if enabled
if (isModuleEnabled('site_builder')) {
setupItems.push({
icon: <GridIcon />,
name: "Your Websites",
name: "Sites",
path: "/sites", // Submenus shown as in-page navigation
});
}
// Add Thinker if enabled
// Add Thinker if enabled (admin only - prompts and AI settings)
if (isModuleEnabled('thinker')) {
setupItems.push({
icon: <BoltIcon />,
name: "AI Writer Setup",
name: "Thinker",
path: "/thinker/prompts", // Default to prompts, submenus shown as in-page navigation
adminOnly: true, // Only visible to admin/staff users
});
}
@@ -96,7 +100,7 @@ const AppSidebar: React.FC = () => {
if (isModuleEnabled('planner')) {
workflowItems.push({
icon: <ListIcon />,
name: "Organize Keywords",
name: "Planner",
path: "/planner/keywords", // Default to keywords, submenus shown as in-page navigation
});
}
@@ -105,7 +109,7 @@ const AppSidebar: React.FC = () => {
if (isModuleEnabled('writer')) {
workflowItems.push({
icon: <TaskIcon />,
name: "Write Articles",
name: "Writer",
path: "/writer/tasks", // Default to tasks, submenus shown as in-page navigation
});
}
@@ -114,7 +118,7 @@ const AppSidebar: React.FC = () => {
if (isModuleEnabled('automation')) {
workflowItems.push({
icon: <BoltIcon />,
name: "Automate Everything",
name: "Automation",
path: "/automation",
});
}
@@ -150,26 +154,21 @@ const AppSidebar: React.FC = () => {
],
},
{
label: "GET STARTED",
label: "SETUP",
items: setupItems,
},
{
label: "CREATE CONTENT",
label: "WORKFLOW",
items: workflowItems,
},
{
label: "MANAGE ACCOUNT",
label: "ACCOUNT",
items: [
{
icon: <UserCircleIcon />,
name: "Account Settings",
path: "/account/settings",
},
{
icon: <UserIcon />,
name: "Team Management",
path: "/account/team",
},
{
icon: <DollarLineIcon />,
name: "Plans & Billing",
@@ -177,42 +176,23 @@ const AppSidebar: React.FC = () => {
},
{
icon: <PieChartIcon />,
name: "Usage & Analytics",
name: "Usage",
path: "/account/usage",
},
],
},
{
label: "CONFIGURATION",
items: [
{
icon: <UserCircleIcon />,
name: "Profile Settings",
path: "/settings/profile",
},
{
icon: <PlugInIcon />,
name: "AI Model Settings",
name: "AI Models",
path: "/settings/integration",
},
{
icon: <PageIcon />,
name: "Publishing",
path: "/settings/publishing",
},
{
icon: <FileIcon />,
name: "Import / Export",
path: "/settings/import-export",
adminOnly: true, // Only visible to admin/staff users
},
],
},
{
label: "HELP & LEARNING",
label: "HELP",
items: [
{
icon: <DocsIcon />,
name: "Help Center",
name: "Help & Docs",
path: "/help",
},
],
@@ -303,7 +283,7 @@ const AppSidebar: React.FC = () => {
};
const renderMenuItems = (items: NavItem[], sectionIndex: number) => (
<ul className="flex flex-col gap-2">
<ul className="flex flex-col gap-0.5">
{items
.filter((nav) => {
// Filter out admin-only items for non-admin users
@@ -450,9 +430,7 @@ const AppSidebar: React.FC = () => {
onMouseEnter={() => !isExpanded && setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div
className={`py-8 flex flex-col justify-center items-center gap-3`}
>
<div className="py-4 flex justify-center items-center">
<Link to="/" className="flex justify-center items-center">
{isExpanded || isHovered || isMobileOpen ? (
<>
@@ -480,23 +458,15 @@ const AppSidebar: React.FC = () => {
/>
)}
</Link>
{/* Version Badge - Only show when sidebar is expanded */}
{(isExpanded || isHovered || isMobileOpen) && (
<div className="flex justify-center items-center">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-900 dark:bg-gray-700 text-gray-100 dark:text-gray-300">
v{APP_VERSION}
</span>
</div>
)}
</div>
<div className="flex flex-col overflow-y-auto duration-300 ease-linear no-scrollbar">
<nav className="mb-6">
<div className="flex flex-col gap-2">
<nav>
<div className="flex flex-col gap-1">
{allSections.map((section, sectionIndex) => (
<div key={section.label || `section-${sectionIndex}`}>
<div key={section.label || `section-${sectionIndex}`} className={section.label ? "mt-4" : ""}>
{section.label && (
<h2
className={`mb-4 text-xs uppercase flex leading-[20px] text-gray-400 ${
className={`mb-2 text-xs font-medium uppercase flex leading-[20px] text-gray-500 dark:text-gray-400 ${
!isExpanded && !isHovered
? "lg:justify-center"
: "justify-start"
@@ -514,7 +484,6 @@ const AppSidebar: React.FC = () => {
))}
</div>
</nav>
{isExpanded || isHovered || isMobileOpen ? <SidebarWidget /> : null}
</div>
</aside>
);

View File

@@ -37,13 +37,13 @@ import {
} from '../../icons';
const STAGE_CONFIG = [
{ icon: ListIcon, color: 'from-blue-500 to-blue-600', textColor: 'text-blue-600', hoverColor: 'hover:border-blue-500', name: 'Organize Keywords', description: 'Group related search terms into topic clusters' },
{ icon: GroupIcon, color: 'from-purple-500 to-purple-600', textColor: 'text-purple-600', hoverColor: 'hover:border-purple-500', name: 'Create Article Ideas', description: 'Generate article titles and outlines for each cluster' },
{ icon: CheckCircleIcon, color: 'from-indigo-500 to-indigo-600', textColor: 'text-indigo-600', hoverColor: 'hover:border-indigo-500', name: 'Prepare Writing Jobs', description: 'Convert ideas into tasks for the AI writer' },
{ icon: PencilIcon, color: 'from-green-500 to-green-600', textColor: 'text-green-600', hoverColor: 'hover:border-green-500', name: 'Write Articles', description: 'AI generates full, complete articles' },
{ icon: FileIcon, color: 'from-amber-500 to-amber-600', textColor: 'text-amber-600', hoverColor: 'hover:border-amber-500', name: 'Create Image Descriptions', description: 'Generate descriptions for AI to create images' },
{ icon: FileTextIcon, color: 'from-pink-500 to-pink-600', textColor: 'text-pink-600', hoverColor: 'hover:border-pink-500', name: 'Generate Images', description: 'AI creates custom images for your articles' },
{ icon: PaperPlaneIcon, color: 'from-teal-500 to-teal-600', textColor: 'text-teal-600', hoverColor: 'hover:border-teal-500', name: 'Review & Publish ⚠️', description: 'Review articles before they go live (manual approval needed)' },
{ icon: ListIcon, color: 'from-blue-500 to-blue-600', textColor: 'text-blue-600', hoverColor: 'hover:border-blue-500', name: 'Keywords → Clusters' },
{ icon: GroupIcon, color: 'from-purple-500 to-purple-600', textColor: 'text-purple-600', hoverColor: 'hover:border-purple-500', name: 'Clusters → Ideas' },
{ icon: CheckCircleIcon, color: 'from-indigo-500 to-indigo-600', textColor: 'text-indigo-600', hoverColor: 'hover:border-indigo-500', name: 'Ideas → Tasks' },
{ icon: PencilIcon, color: 'from-green-500 to-green-600', textColor: 'text-green-600', hoverColor: 'hover:border-green-500', name: 'Tasks → Content' },
{ icon: FileIcon, color: 'from-amber-500 to-amber-600', textColor: 'text-amber-600', hoverColor: 'hover:border-amber-500', name: 'Content → Image Prompts' },
{ icon: FileTextIcon, color: 'from-pink-500 to-pink-600', textColor: 'text-pink-600', hoverColor: 'hover:border-pink-500', name: 'Image Prompts → Images' },
{ icon: PaperPlaneIcon, color: 'from-teal-500 to-teal-600', textColor: 'text-teal-600', hoverColor: 'hover:border-teal-500', name: 'Review Gate' },
];
const AutomationPage: React.FC = () => {
@@ -389,7 +389,7 @@ const AutomationPage: React.FC = () => {
<BoltIcon className="text-white size-5" />
</div>
<div>
<h2 className="text-2xl font-bold text-gray-800 dark:text-white/90">Automate Everything</h2>
<h2 className="text-2xl font-bold text-gray-800 dark:text-white/90">Automation</h2>
{activeSite && (
<p className="text-sm text-gray-500 dark:text-gray-400 mt-0.5">
Site: <span className="font-medium text-brand-600 dark:text-brand-400">{activeSite.name}</span>
@@ -411,7 +411,7 @@ const AutomationPage: React.FC = () => {
<div className="text-sm font-semibold text-slate-900 dark:text-white truncate">
{currentRun?.status === 'running' && `Running - Stage ${currentRun.current_stage}/7`}
{currentRun?.status === 'paused' && 'Paused'}
{!currentRun && totalPending > 0 && 'Ready to Go!'}
{!currentRun && totalPending > 0 && 'Ready to Run'}
{!currentRun && totalPending === 0 && 'No Items Pending'}
</div>
<div className="text-xs text-slate-600 dark:text-gray-400 truncate">

View File

@@ -595,10 +595,10 @@ export default function Home() {
{/* Custom Header with Site Selector and Refresh */}
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-6">
<div className="flex-1">
<h2 className="text-2xl font-bold text-gray-800 dark:text-white/90">Your Content Creation Dashboard</h2>
<h2 className="text-2xl font-bold text-gray-800 dark:text-white/90">Dashboard</h2>
{lastUpdated && (
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
Last checked: {lastUpdated.toLocaleTimeString()}
Last updated: {lastUpdated.toLocaleTimeString()}
</p>
)}
</div>
@@ -629,12 +629,9 @@ export default function Home() {
<h1 className="text-3xl md:text-4xl font-bold mb-2">
AI-Powered Content Creation Workflow
</h1>
<p className="text-lg text-white/90 mb-1">
<p className="text-lg text-white/90">
Transform keywords into published content with intelligent automation.
</p>
<p className="text-sm text-white/80">
Your complete toolkit for finding topics, creating content, and publishing it to your site - all automated
</p>
</div>
{/* Add Site Button and Site Count in Single Row - Right Side */}
<div className="flex items-center gap-4">
@@ -642,7 +639,7 @@ export default function Home() {
<div className="text-right">
{sites.length > 1 ? (
<div className="text-3xl font-bold text-white">
{sites.length} of {maxSites || '∞'} Sites Active
{sites.length}/{maxSites || '∞'} Sites
</div>
) : (
<div className="text-xl font-semibold text-white/90">
@@ -661,7 +658,7 @@ export default function Home() {
startIcon={<PlusIcon className="w-6 h-6 fill-current" />}
className="!bg-white !text-brand-600 hover:!bg-gray-50 font-bold text-base px-8 py-4 shadow-2xl hover:shadow-[0_25px_50px_-12px_rgba(0,0,0,0.4)] hover:scale-105 active:scale-100 transition-all duration-200"
>
+ Add Another Website
Add Site
</Button>
)}
{!canAddMoreSites && sites.length > 0 && maxSites > 0 && (
@@ -685,7 +682,7 @@ export default function Home() {
<div className="space-y-6">
{/* Progress Flow - Circular Design with Progress Bar */}
<ComponentCard title="Your Content Journey" desc="Track your content creation progress from ideas to published articles">
<ComponentCard title="Workflow Progress" desc="Track your content creation pipeline">
{/* Percentage and Progress Bar */}
<div className="mb-8">
<div className="flex items-center justify-between mb-3">
@@ -702,9 +699,6 @@ export default function Home() {
color="primary"
className="h-4"
/>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
(This shows your progress from keywords through to published content)
</p>
</div>
{/* Icon-based Progress Flow */}
@@ -717,7 +711,7 @@ export default function Home() {
<div className="text-center">
<div className="text-sm font-semibold text-gray-800 dark:text-white">Site & Sectors</div>
<div className="text-lg font-bold text-gray-900 dark:text-white">{sites.filter(s => s.active_sectors_count > 0).length}</div>
<div className="text-xs text-gray-500 dark:text-gray-400">Niches you're targeting - Industry & sectors set up</div>
<div className="text-xs text-gray-500 dark:text-gray-400">Industry & sectors configured</div>
</div>
</Link>
@@ -728,7 +722,7 @@ export default function Home() {
<div className="text-center">
<div className="text-sm font-semibold text-gray-800">Keywords</div>
<div className="text-lg font-bold text-blue-600">{progress.keywordsCount}</div>
<div className="text-xs text-gray-500">Search terms to target - Keywords added from research</div>
<div className="text-xs text-gray-500">Added from opportunities</div>
</div>
</Link>
@@ -739,7 +733,7 @@ export default function Home() {
<div className="text-center">
<div className="text-sm font-semibold text-gray-800">Clusters</div>
<div className="text-lg font-bold text-purple-600">{progress.clustersCount}</div>
<div className="text-xs text-gray-500">Topic groups - Keywords organized by theme</div>
<div className="text-xs text-gray-500">Keywords grouped by topic</div>
</div>
</Link>
@@ -750,7 +744,7 @@ export default function Home() {
<div className="text-center">
<div className="text-sm font-semibold text-gray-800">Ideas</div>
<div className="text-lg font-bold text-orange-600">{progress.ideasCount}</div>
<div className="text-xs text-gray-500">Article outlines ready - Ideas and outlines created</div>
<div className="text-xs text-gray-500">Content ideas and outlines</div>
</div>
</Link>
@@ -761,7 +755,7 @@ export default function Home() {
<div className="text-center">
<div className="text-sm font-semibold text-gray-800">Content</div>
<div className="text-lg font-bold text-green-600">{progress.contentCount}</div>
<div className="text-xs text-gray-500">Articles created - Written content + images ready</div>
<div className="text-xs text-gray-500">Articles ready to publish</div>
</div>
</Link>
@@ -772,7 +766,7 @@ export default function Home() {
<div className="text-center">
<div className="text-sm font-semibold text-gray-800">Published</div>
<div className="text-lg font-bold text-indigo-600">{progress.publishedCount}</div>
<div className="text-xs text-gray-500">Live on your site - Articles published and active</div>
<div className="text-xs text-gray-500">Live on your site</div>
</div>
</Link>
</div>
@@ -859,7 +853,7 @@ export default function Home() {
<EnhancedMetricCard
title="Your Keywords"
value={insights?.totalKeywords.toLocaleString() || "0"}
subtitle={`Organized into ${insights?.totalClusters || 0} topic groups with ${insights?.totalIdeas || 0} content ideas`}
subtitle={`Organized into ${insights?.totalClusters || 0} clusters with ${insights?.totalIdeas || 0} content ideas`}
icon={<ListIcon className="size-6" />}
accentColor="blue"
trend={0}

View File

@@ -444,15 +444,16 @@ export default function Clusters() {
// Planner navigation tabs
const plannerTabs = [
{ label: 'Keywords (individual terms)', path: '/planner/keywords', icon: <ListIcon /> },
{ label: 'Topics (keyword groups)', path: '/planner/clusters', icon: <GroupIcon /> },
{ label: 'Keywords', path: '/planner/keywords', icon: <ListIcon /> },
{ label: 'Clusters', path: '/planner/clusters', icon: <GroupIcon /> },
{ label: 'Ideas', path: '/planner/ideas', icon: <BoltIcon /> },
];
return (
<>
<PageHeader
title="Topic Clusters"
title="Clusters"
description="Keyword groups organized by topic. Generate content ideas from clusters to build topical authority."
badge={{ icon: <GroupIcon />, color: 'purple' }}
navigation={<ModuleNavigationTabs tabs={plannerTabs} />}
workflowInsights={workflowInsights}

View File

@@ -350,15 +350,16 @@ export default function Ideas() {
// Planner navigation tabs
const plannerTabs = [
{ label: 'Keywords (individual terms)', path: '/planner/keywords', icon: <ListIcon /> },
{ label: 'Topics (keyword groups)', path: '/planner/clusters', icon: <GroupIcon /> },
{ label: 'Keywords', path: '/planner/keywords', icon: <ListIcon /> },
{ label: 'Clusters', path: '/planner/clusters', icon: <GroupIcon /> },
{ label: 'Ideas', path: '/planner/ideas', icon: <BoltIcon /> },
];
return (
<>
<PageHeader
title="Article Ideas"
title="Ideas"
description="AI-generated content ideas with titles, outlines, and target keywords. Queue ideas to start content generation."
badge={{ icon: <BoltIcon />, color: 'orange' }}
navigation={<ModuleNavigationTabs tabs={plannerTabs} />}
workflowInsights={workflowInsights}
@@ -474,7 +475,7 @@ export default function Ideas() {
{
title: 'Clusters',
value: clusters.length.toLocaleString(),
subtitle: 'topic groups',
subtitle: 'keyword groups',
icon: <GroupIcon className="w-5 h-5" />,
accentColor: 'purple',
href: '/planner/clusters',

View File

@@ -611,15 +611,16 @@ export default function Keywords() {
// Planner navigation tabs
const plannerTabs = [
{ label: 'Keywords (individual terms)', path: '/planner/keywords', icon: <ListIcon /> },
{ label: 'Topics (keyword groups)', path: '/planner/clusters', icon: <GroupIcon /> },
{ label: 'Keywords', path: '/planner/keywords', icon: <ListIcon /> },
{ label: 'Clusters', path: '/planner/clusters', icon: <GroupIcon /> },
{ label: 'Ideas', path: '/planner/ideas', icon: <BoltIcon /> },
];
return (
<>
<PageHeader
title="Your Keywords"
title="Keywords"
description="Your target search terms organized for content creation. Import, cluster, and transform into content ideas."
badge={{ icon: <ListIcon />, color: 'green' }}
navigation={<ModuleNavigationTabs tabs={plannerTabs} />}
workflowInsights={workflowInsights}

View File

@@ -280,17 +280,18 @@ export default function Content() {
// Writer navigation tabs
const writerTabs = [
{ label: 'Ready to Write', path: '/writer/tasks', icon: <TaskIcon /> },
{ label: 'Finished Drafts', path: '/writer/content', icon: <FileIcon /> },
{ label: 'Article Images', path: '/writer/images', icon: <ImageIcon /> },
{ label: 'Review Before Publishing', path: '/writer/review', icon: <CheckCircleIcon /> },
{ label: 'Queue', path: '/writer/tasks', icon: <TaskIcon /> },
{ label: 'Drafts', path: '/writer/content', icon: <FileIcon /> },
{ label: 'Images', path: '/writer/images', icon: <ImageIcon /> },
{ label: 'Review', path: '/writer/review', icon: <CheckCircleIcon /> },
{ label: 'Published', path: '/writer/published', icon: <CheckCircleIcon /> },
];
return (
<>
<PageHeader
title="Your Articles"
title="Drafts"
description="AI-generated content ready for review. Add images, edit, and publish when ready."
badge={{ icon: <FileIcon />, color: 'purple' }}
navigation={<ModuleNavigationTabs tabs={writerTabs} />}
workflowInsights={workflowInsights}

View File

@@ -449,17 +449,17 @@ export default function Images() {
// Writer navigation tabs
const writerTabs = [
{ label: 'Ready to Write', path: '/writer/tasks', icon: <TaskIcon /> },
{ label: 'Finished Drafts', path: '/writer/content', icon: <FileIcon /> },
{ label: 'Article Images', path: '/writer/images', icon: <ImageIcon /> },
{ label: 'Review Before Publishing', path: '/writer/review', icon: <CheckCircleIcon /> },
{ label: 'Queue', path: '/writer/tasks', icon: <TaskIcon /> },
{ label: 'Drafts', path: '/writer/content', icon: <FileIcon /> },
{ label: 'Images', path: '/writer/images', icon: <ImageIcon /> },
{ label: 'Review', path: '/writer/review', icon: <CheckCircleIcon /> },
{ label: 'Published', path: '/writer/published', icon: <CheckCircleIcon /> },
];
return (
<>
<PageHeader
title="Article Images"
title="Images"
badge={{ icon: <FileIcon />, color: 'orange' }}
navigation={<ModuleNavigationTabs tabs={writerTabs} />}
/>

View File

@@ -307,17 +307,17 @@ export default function Published() {
// Writer navigation tabs
const writerTabs = [
{ label: 'Ready to Write', path: '/writer/tasks', icon: <TaskIcon /> },
{ label: 'Finished Drafts', path: '/writer/content', icon: <FileIcon /> },
{ label: 'Article Images', path: '/writer/images', icon: <ImageIcon /> },
{ label: 'Review Before Publishing', path: '/writer/review', icon: <CheckCircleIcon /> },
{ label: 'Queue', path: '/writer/tasks', icon: <TaskIcon /> },
{ label: 'Drafts', path: '/writer/content', icon: <FileIcon /> },
{ label: 'Images', path: '/writer/images', icon: <ImageIcon /> },
{ label: 'Review', path: '/writer/review', icon: <CheckCircleIcon /> },
{ label: 'Published', path: '/writer/published', icon: <CheckCircleIcon /> },
];
return (
<>
<PageHeader
title="Published Articles"
title="Published"
badge={{ icon: <CheckCircleIcon />, color: 'green' }}
navigation={<ModuleNavigationTabs tabs={writerTabs} />}
/>

View File

@@ -346,10 +346,10 @@ export default function Review() {
// Writer navigation tabs
const writerTabs = [
{ label: 'Ready to Write', path: '/writer/tasks', icon: <TaskIcon /> },
{ label: 'Finished Drafts', path: '/writer/content', icon: <FileIcon /> },
{ label: 'Article Images', path: '/writer/images', icon: <ImageIcon /> },
{ label: 'Review Before Publishing', path: '/writer/review', icon: <CheckCircleIcon /> },
{ label: 'Queue', path: '/writer/tasks', icon: <TaskIcon /> },
{ label: 'Drafts', path: '/writer/content', icon: <FileIcon /> },
{ label: 'Images', path: '/writer/images', icon: <ImageIcon /> },
{ label: 'Review', path: '/writer/review', icon: <CheckCircleIcon /> },
{ label: 'Published', path: '/writer/published', icon: <CheckCircleIcon /> },
];

View File

@@ -424,17 +424,18 @@ export default function Tasks() {
// Writer navigation tabs
const writerTabs = [
{ label: 'Ready to Write', path: '/writer/tasks', icon: <TaskIcon /> },
{ label: 'Finished Drafts', path: '/writer/content', icon: <FileIcon /> },
{ label: 'Article Images', path: '/writer/images', icon: <ImageIcon /> },
{ label: 'Review Before Publishing', path: '/writer/review', icon: <CheckCircleIcon /> },
{ label: 'Queue', path: '/writer/tasks', icon: <TaskIcon /> },
{ label: 'Drafts', path: '/writer/content', icon: <FileIcon /> },
{ label: 'Images', path: '/writer/images', icon: <ImageIcon /> },
{ label: 'Review', path: '/writer/review', icon: <CheckCircleIcon /> },
{ label: 'Published', path: '/writer/published', icon: <CheckCircleIcon /> },
];
return (
<>
<PageHeader
title="Writing Tasks"
title="Content Queue"
description="Manage content tasks waiting for AI generation. Queue ideas here to create articles automatically."
badge={{ icon: <TaskIcon />, color: 'indigo' }}
navigation={<ModuleNavigationTabs tabs={writerTabs} />}
workflowInsights={workflowInsights}

View File

@@ -1,25 +1,41 @@
/**
* Account Settings Page
* Manage account information and billing address
* Account Settings Page - Consolidated Settings
* Tabs: Account, Profile, Team
* Consistent with system page structure (like Plans & Usage pages)
*/
import { useState, useEffect } from 'react';
import { Save, Loader2 } from 'lucide-react';
import {
Save, Loader2, Settings, User, Users, UserPlus, Shield, Lock
} from 'lucide-react';
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
import Badge from '../../components/ui/badge/Badge';
import PageMeta from '../../components/common/PageMeta';
import { useToast } from '../../components/ui/toast/ToastContainer';
import {
getAccountSettings,
updateAccountSettings,
getTeamMembers,
inviteTeamMember,
removeTeamMember,
type AccountSettings,
type TeamMember,
} from '../../services/billing.api';
type TabType = 'account' | 'profile' | 'team';
export default function AccountSettingsPage() {
const [settings, setSettings] = useState<AccountSettings | null>(null);
const toast = useToast();
const [activeTab, setActiveTab] = useState<TabType>('account');
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string>('');
const [success, setSuccess] = useState<string>('');
const [formData, setFormData] = useState({
// Account settings state
const [settings, setSettings] = useState<AccountSettings | null>(null);
const [accountForm, setAccountForm] = useState({
name: '',
billing_address_line1: '',
billing_address_line2: '',
@@ -31,252 +47,730 @@ export default function AccountSettingsPage() {
billing_email: '',
});
// Profile settings state
const [profileForm, setProfileForm] = useState({
firstName: '',
lastName: '',
email: '',
phone: '',
timezone: 'America/New_York',
language: 'en',
emailNotifications: true,
marketingEmails: false,
});
// Team state
const [members, setMembers] = useState<TeamMember[]>([]);
const [teamLoading, setTeamLoading] = useState(false);
const [showInviteModal, setShowInviteModal] = useState(false);
const [inviting, setInviting] = useState(false);
const [inviteForm, setInviteForm] = useState({
email: '',
first_name: '',
last_name: '',
});
useEffect(() => {
loadSettings();
loadData();
}, []);
const loadSettings = async () => {
const loadData = async () => {
try {
setLoading(true);
const data = await getAccountSettings();
setSettings(data);
setFormData({
name: data.name || '',
billing_address_line1: data.billing_address_line1 || '',
billing_address_line2: data.billing_address_line2 || '',
billing_city: data.billing_city || '',
billing_state: data.billing_state || '',
billing_postal_code: data.billing_postal_code || '',
billing_country: data.billing_country || '',
tax_id: data.tax_id || '',
billing_email: data.billing_email || '',
const accountData = await getAccountSettings();
setSettings(accountData);
setAccountForm({
name: accountData.name || '',
billing_address_line1: accountData.billing_address_line1 || '',
billing_address_line2: accountData.billing_address_line2 || '',
billing_city: accountData.billing_city || '',
billing_state: accountData.billing_state || '',
billing_postal_code: accountData.billing_postal_code || '',
billing_country: accountData.billing_country || '',
tax_id: accountData.tax_id || '',
billing_email: accountData.billing_email || '',
});
} catch (err: any) {
setError(err.message || 'Failed to load account settings');
console.error('Account settings load error:', err);
setError(err.message || 'Failed to load settings');
} finally {
setLoading(false);
}
};
const handleSubmit = async (e: React.FormEvent) => {
const loadTeamMembers = async () => {
try {
setTeamLoading(true);
const data = await getTeamMembers();
setMembers(data.results || []);
} catch (error: any) {
toast.error(`Failed to load team members: ${error.message}`);
} finally {
setTeamLoading(false);
}
};
// Load team members when team tab is selected
useEffect(() => {
if (activeTab === 'team' && members.length === 0) {
loadTeamMembers();
}
}, [activeTab]);
const handleAccountSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
setSaving(true);
setError('');
setSuccess('');
await updateAccountSettings(formData);
await updateAccountSettings(accountForm);
setSuccess('Account settings updated successfully');
await loadSettings();
toast.success('Account settings saved');
await loadData();
} catch (err: any) {
setError(err.message || 'Failed to update account settings');
toast.error(err.message || 'Failed to save settings');
} finally {
setSaving(false);
}
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData(prev => ({
const handleProfileSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
setSaving(true);
// TODO: Connect to profile API when available
await new Promise(resolve => setTimeout(resolve, 500));
toast.success('Profile settings saved');
} catch (err: any) {
toast.error(err.message || 'Failed to save profile');
} finally {
setSaving(false);
}
};
const handleInvite = async () => {
if (!inviteForm.email) {
toast.error('Email is required');
return;
}
try {
setInviting(true);
const result = await inviteTeamMember(inviteForm);
toast.success(result.message || 'Team member invited successfully');
setShowInviteModal(false);
setInviteForm({ email: '', first_name: '', last_name: '' });
await loadTeamMembers();
} catch (error: any) {
toast.error(`Failed to invite team member: ${error.message}`);
} finally {
setInviting(false);
}
};
const handleRemoveMember = async (userId: number, email: string) => {
if (!confirm(`Are you sure you want to remove ${email} from the team?`)) {
return;
}
try {
const result = await removeTeamMember(userId);
toast.success(result.message || 'Team member removed successfully');
await loadTeamMembers();
} catch (error: any) {
toast.error(`Failed to remove team member: ${error.message}`);
}
};
const handleAccountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setAccountForm(prev => ({
...prev,
[e.target.name]: e.target.value
}));
};
const tabs = [
{ id: 'account' as TabType, label: 'Account', icon: <Settings className="w-4 h-4" /> },
{ id: 'profile' as TabType, label: 'Profile', icon: <User className="w-4 h-4" /> },
{ id: 'team' as TabType, label: 'Team', icon: <Users className="w-4 h-4" /> },
];
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<Loader2 className="w-8 h-8 animate-spin text-[var(--color-brand-500)]" />
<div className="p-6">
<PageMeta title="Account Settings" description="Manage your account, profile, and team" />
<div className="flex items-center justify-center h-64">
<div className="flex flex-col items-center gap-3">
<Loader2 className="w-8 h-8 animate-spin text-[var(--color-brand-500)]" />
<div className="text-gray-500 dark:text-gray-400">Loading settings...</div>
</div>
</div>
</div>
);
}
return (
<div className="p-6 max-w-4xl mx-auto">
<div className="p-6">
<PageMeta title="Account Settings" description="Manage your account, profile, and team" />
{/* Page Header */}
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Account Settings</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Manage your account information and billing details
Manage your account information, profile settings, and team members
</p>
</div>
{error && (
<div className="mb-6 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<p className="text-red-800 dark:text-red-200">{error}</p>
</div>
)}
{/* Tabs */}
<div className="mb-6 border-b border-gray-200 dark:border-gray-700">
<nav className="-mb-px flex space-x-8">
{tabs.map((tab) => (
<button
type="button"
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`
flex items-center gap-2 py-4 px-1 border-b-2 font-medium text-sm
${activeTab === tab.id
? 'border-[var(--color-brand-500)] text-[var(--color-brand-600)] dark:text-[var(--color-brand-400)]'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
}
`}
>
{tab.icon}
{tab.label}
</button>
))}
</nav>
</div>
{success && (
<div className="mb-6 p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
<p className="text-green-800 dark:text-green-200">{success}</p>
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
{/* Account Information */}
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4">Account Information</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Account Name
</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-800"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Account Slug
</label>
<input
type="text"
value={settings?.slug || ''}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-100 dark:bg-gray-700"
disabled
/>
</div>
</div>
<div className="mt-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Billing Email
</label>
<input
type="email"
name="billing_email"
value={formData.billing_email}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-800"
/>
</div>
</Card>
{/* Billing Address */}
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4">Billing Address</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Address Line 1
</label>
<input
type="text"
name="billing_address_line1"
value={formData.billing_address_line1}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-800"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Address Line 2
</label>
<input
type="text"
name="billing_address_line2"
value={formData.billing_address_line2}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-800"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
City
</label>
<input
type="text"
name="billing_city"
value={formData.billing_city}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-800"
/>
{/* Tab Content */}
<div className="mt-6">
{/* Account Tab */}
{activeTab === 'account' && (
<div className="space-y-6 max-w-4xl">
{error && (
<div className="p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<p className="text-red-800 dark:text-red-200">{error}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
State/Province
</label>
<input
type="text"
name="billing_state"
value={formData.billing_state}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-800"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Postal Code
</label>
<input
type="text"
name="billing_postal_code"
value={formData.billing_postal_code}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-800"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Country
</label>
<input
type="text"
name="billing_country"
value={formData.billing_country}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-800"
placeholder="US, GB, IN, etc."
/>
</div>
</div>
</Card>
{/* Tax Information */}
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4">Tax Information</h2>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Tax ID / VAT Number
</label>
<input
type="text"
name="tax_id"
value={formData.tax_id}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-gray-800"
placeholder="Optional"
/>
</div>
</Card>
{/* Submit Button */}
<div className="flex justify-end">
<button
type="submit"
disabled={saving}
className="flex items-center gap-2 px-6 py-2 bg-[var(--color-brand-500)] text-white rounded-lg hover:bg-[var(--color-brand-600)] disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{saving ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Saving...
</>
) : (
<>
<Save className="w-4 h-4" />
Save Changes
</>
)}
</button>
{success && (
<div className="p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
<p className="text-green-800 dark:text-green-200">{success}</p>
</div>
)}
<form onSubmit={handleAccountSubmit} className="space-y-6">
{/* Account Information */}
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Account Information</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Account Name
</label>
<input
type="text"
name="name"
value={accountForm.name}
onChange={handleAccountChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Account Slug
</label>
<input
type="text"
value={settings?.slug || ''}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-100 dark:bg-gray-700"
disabled
/>
</div>
</div>
<div className="mt-4">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Billing Email
</label>
<input
type="email"
name="billing_email"
value={accountForm.billing_email}
onChange={handleAccountChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
/>
</div>
</Card>
{/* Billing Address */}
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Billing Address</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Address Line 1
</label>
<input
type="text"
name="billing_address_line1"
value={accountForm.billing_address_line1}
onChange={handleAccountChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Address Line 2
</label>
<input
type="text"
name="billing_address_line2"
value={accountForm.billing_address_line2}
onChange={handleAccountChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
City
</label>
<input
type="text"
name="billing_city"
value={accountForm.billing_city}
onChange={handleAccountChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
State/Province
</label>
<input
type="text"
name="billing_state"
value={accountForm.billing_state}
onChange={handleAccountChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Postal Code
</label>
<input
type="text"
name="billing_postal_code"
value={accountForm.billing_postal_code}
onChange={handleAccountChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Country
</label>
<input
type="text"
name="billing_country"
value={accountForm.billing_country}
onChange={handleAccountChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
placeholder="US, GB, IN, etc."
/>
</div>
</div>
</Card>
{/* Tax Information */}
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Tax Information</h2>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Tax ID / VAT Number
</label>
<input
type="text"
name="tax_id"
value={accountForm.tax_id}
onChange={handleAccountChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
placeholder="Optional"
/>
</div>
</Card>
{/* Submit Button */}
<div className="flex justify-end">
<Button
type="submit"
variant="primary"
tone="brand"
disabled={saving}
startIcon={saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
>
{saving ? 'Saving...' : 'Save Changes'}
</Button>
</div>
</form>
</div>
)}
{/* Profile Tab */}
{activeTab === 'profile' && (
<div className="space-y-6 max-w-4xl">
<form onSubmit={handleProfileSubmit} className="space-y-6">
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">About You</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
First Name
</label>
<input
type="text"
value={profileForm.firstName}
onChange={(e) => setProfileForm({ ...profileForm, firstName: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Last Name
</label>
<input
type="text"
value={profileForm.lastName}
onChange={(e) => setProfileForm({ ...profileForm, lastName: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Email
</label>
<input
type="email"
value={profileForm.email}
onChange={(e) => setProfileForm({ ...profileForm, email: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Phone Number (optional)
</label>
<input
type="tel"
value={profileForm.phone}
onChange={(e) => setProfileForm({ ...profileForm, phone: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
/>
</div>
</div>
</Card>
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Preferences</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Your Timezone
</label>
<select
value={profileForm.timezone}
onChange={(e) => setProfileForm({ ...profileForm, timezone: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
>
<option value="America/New_York">Eastern Time</option>
<option value="America/Chicago">Central Time</option>
<option value="America/Denver">Mountain Time</option>
<option value="America/Los_Angeles">Pacific Time</option>
<option value="UTC">UTC</option>
<option value="Europe/London">London</option>
<option value="Asia/Kolkata">India</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Language
</label>
<select
value={profileForm.language}
onChange={(e) => setProfileForm({ ...profileForm, language: e.target.value })}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800"
>
<option value="en">English</option>
<option value="es">Spanish</option>
<option value="fr">French</option>
</select>
</div>
</div>
</Card>
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Notifications</h2>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
Choose what emails you want to receive:
</p>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-gray-900 dark:text-white">Important Updates</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
Get notified about important changes to your account
</div>
</div>
<input
type="checkbox"
checked={profileForm.emailNotifications}
onChange={(e) => setProfileForm({ ...profileForm, emailNotifications: e.target.checked })}
className="w-5 h-5 text-[var(--color-brand-500)] rounded focus:ring-[var(--color-brand-500)]"
/>
</div>
<div className="flex items-center justify-between">
<div>
<div className="font-medium text-gray-900 dark:text-white">Tips & Product Updates</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
Hear about new features and content tips
</div>
</div>
<input
type="checkbox"
checked={profileForm.marketingEmails}
onChange={(e) => setProfileForm({ ...profileForm, marketingEmails: e.target.checked })}
className="w-5 h-5 text-[var(--color-brand-500)] rounded focus:ring-[var(--color-brand-500)]"
/>
</div>
</div>
</Card>
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white flex items-center gap-2">
<Lock className="w-5 h-5" />
Security
</h2>
<Button variant="outline" tone="neutral">
Change Password
</Button>
</Card>
{/* Submit Button */}
<div className="flex justify-end">
<Button
type="submit"
variant="primary"
tone="brand"
disabled={saving}
startIcon={saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
>
{saving ? 'Saving...' : 'Save Profile'}
</Button>
</div>
</form>
</div>
)}
{/* Team Tab */}
{activeTab === 'team' && (
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Team Members</h2>
<p className="text-sm text-gray-600 dark:text-gray-400">
Manage who can access your account
</p>
</div>
<Button
variant="primary"
tone="brand"
startIcon={<UserPlus className="w-4 h-4" />}
onClick={() => setShowInviteModal(true)}
>
Invite Someone
</Button>
</div>
{teamLoading ? (
<div className="flex items-center justify-center h-32">
<Loader2 className="w-6 h-6 animate-spin text-[var(--color-brand-500)]" />
</div>
) : (
<Card className="overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<tr>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Name</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Email</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Status</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Role</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Joined</th>
<th className="text-right py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
{members.map((member) => (
<tr key={member.id} className="hover:bg-gray-50 dark:hover:bg-gray-800">
<td className="py-3 px-4 text-sm text-gray-900 dark:text-white">
{member.first_name || member.last_name
? `${member.first_name} ${member.last_name}`.trim()
: '-'}
</td>
<td className="py-3 px-4 text-sm text-gray-900 dark:text-white">
{member.email}
</td>
<td className="py-3 px-4">
<Badge
variant="light"
color={member.is_active ? 'success' : 'error'}
>
{member.is_active ? 'Active' : 'Inactive'}
</Badge>
</td>
<td className="py-3 px-4 text-sm text-gray-600 dark:text-gray-400">
{member.is_staff ? 'Admin' : 'Member'}
</td>
<td className="py-3 px-4 text-sm text-gray-600 dark:text-gray-400">
{member.date_joined ? new Date(member.date_joined).toLocaleDateString() : 'N/A'}
</td>
<td className="py-3 px-4 text-right">
<Button
variant="outline"
tone="neutral"
size="sm"
onClick={() => handleRemoveMember(member.id, member.email)}
>
Remove
</Button>
</td>
</tr>
))}
{members.length === 0 && (
<tr>
<td colSpan={6} className="py-12 text-center text-gray-500 dark:text-gray-400">
<Users className="w-12 h-12 mx-auto mb-3 text-gray-300 dark:text-gray-600" />
No team members yet. Invite your first team member!
</td>
</tr>
)}
</tbody>
</table>
</div>
</Card>
)}
{/* Role Permissions Info */}
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white flex items-center gap-2">
<Shield className="w-5 h-5" />
Role Permissions
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
<div className="flex items-center justify-between mb-2">
<h4 className="font-semibold text-gray-900 dark:text-white">Admin</h4>
<Badge variant="light" color="primary">High Access</Badge>
</div>
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<li> Manage all sites and content</li>
<li> Invite team members</li>
<li> Cannot manage billing</li>
</ul>
</div>
<div className="p-4 border border-gray-200 dark:border-gray-700 rounded-lg">
<div className="flex items-center justify-between mb-2">
<h4 className="font-semibold text-gray-900 dark:text-white">Member</h4>
<Badge variant="light" color="info">Standard Access</Badge>
</div>
<ul className="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<li> Create and edit content</li>
<li> View analytics</li>
<li> Cannot invite users</li>
</ul>
</div>
</div>
</Card>
</div>
)}
</div>
{/* Invite Modal */}
{showInviteModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<Card className="p-6 w-full max-w-md mx-4">
<h2 className="text-xl font-bold text-gray-900 dark:text-white mb-4">
Invite Team Member
</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Email *
</label>
<input
type="email"
value={inviteForm.email}
onChange={(e) => setInviteForm(prev => ({ ...prev, email: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
placeholder="user@example.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
First Name
</label>
<input
type="text"
value={inviteForm.first_name}
onChange={(e) => setInviteForm(prev => ({ ...prev, first_name: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Last Name
</label>
<input
type="text"
value={inviteForm.last_name}
onChange={(e) => setInviteForm(prev => ({ ...prev, last_name: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-white"
/>
</div>
</div>
<div className="mt-6 flex justify-end gap-3">
<Button
variant="outline"
tone="neutral"
onClick={() => {
setShowInviteModal(false);
setInviteForm({ email: '', first_name: '', last_name: '' });
}}
disabled={inviting}
>
Cancel
</Button>
<Button
variant="primary"
tone="brand"
onClick={handleInvite}
disabled={inviting}
>
{inviting ? 'Inviting...' : 'Send Invitation'}
</Button>
</div>
</Card>
</div>
</form>
)}
</div>
);
}

View File

@@ -0,0 +1,616 @@
/**
* Content Settings Page - 3 Tabs
* Tabs: Content Generation, Publishing, Image Settings
* Consolidated settings for content creation workflow
*/
import { useState, useEffect, useCallback } from 'react';
import {
Save, Loader2, Image as ImageIcon, FileText, Send, Settings
} from 'lucide-react';
import { Card } from '../../components/ui/card';
import Button from '../../components/ui/button/Button';
import { fetchAPI } from '../../services/api';
import { useToast } from '../../components/ui/toast/ToastContainer';
import SelectDropdown from '../../components/form/SelectDropdown';
import Label from '../../components/form/Label';
import Checkbox from '../../components/form/input/Checkbox';
import PageMeta from '../../components/common/PageMeta';
type TabType = 'content' | 'publishing' | 'images';
interface ImageGenerationSettings {
enabled: boolean;
service: 'openai' | 'runware';
provider: string;
model: string;
runwareModel?: string;
image_type: 'realistic' | 'artistic' | 'cartoon';
max_in_article_images: number;
image_format: 'webp' | 'jpg' | 'png';
desktop_enabled: boolean;
mobile_enabled: boolean;
featured_image_size: string;
desktop_image_size: string;
}
interface PublishingSettings {
autoPublishEnabled: boolean;
autoSyncEnabled: boolean;
}
interface ContentGenerationSettings {
appendToPrompt: string;
defaultTone: string;
defaultLength: string;
}
// Map user-friendly quality to internal service/model configuration
const QUALITY_TO_CONFIG: Record<string, { service: 'openai' | 'runware'; model: string }> = {
standard: { service: 'openai', model: 'dall-e-2' },
premium: { service: 'openai', model: 'dall-e-3' },
best: { service: 'runware', model: 'runware:97@1' },
};
// Map internal config back to user-friendly quality
const getQualityFromConfig = (service?: string, model?: string): 'standard' | 'premium' | 'best' => {
if (service === 'runware') return 'best';
if (model === 'dall-e-3') return 'premium';
return 'standard';
};
// Get available image sizes based on provider and model
const getImageSizes = (provider: string, model: string) => {
if (provider === 'runware') {
return [
{ value: '1280x832', label: '1280×832 pixels' },
{ value: '1024x1024', label: '1024×1024 pixels' },
{ value: '512x512', label: '512×512 pixels' },
];
} else if (provider === 'openai') {
if (model === 'dall-e-2') {
return [
{ value: '256x256', label: '256×256 pixels' },
{ value: '512x512', label: '512×512 pixels' },
{ value: '1024x1024', label: '1024×1024 pixels' },
];
} else if (model === 'dall-e-3') {
return [
{ value: '1024x1024', label: '1024×1024 pixels' },
];
}
}
return [{ value: '1024x1024', label: '1024×1024 pixels' }];
};
export default function ContentSettingsPage() {
const toast = useToast();
const [activeTab, setActiveTab] = useState<TabType>('content');
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
// Content Generation Settings
const [contentSettings, setContentSettings] = useState<ContentGenerationSettings>({
appendToPrompt: '',
defaultTone: 'professional',
defaultLength: 'medium',
});
// Publishing Settings
const [publishingSettings, setPublishingSettings] = useState<PublishingSettings>({
autoPublishEnabled: false,
autoSyncEnabled: false,
});
// Image Quality
const [imageQuality, setImageQuality] = useState<'standard' | 'premium' | 'best'>('premium');
// Image Generation Settings
const [imageSettings, setImageSettings] = useState<ImageGenerationSettings>({
enabled: true,
service: 'openai',
provider: 'openai',
model: 'dall-e-3',
image_type: 'realistic',
max_in_article_images: 2,
image_format: 'webp',
desktop_enabled: true,
mobile_enabled: true,
featured_image_size: '1024x1024',
desktop_image_size: '1024x1024',
});
// Get current provider/model from quality setting
const getCurrentConfig = useCallback(() => {
const config = QUALITY_TO_CONFIG[imageQuality];
return {
service: config.service,
model: config.model,
};
}, [imageQuality]);
// Get available sizes for current quality
const availableSizes = getImageSizes(
getCurrentConfig().service,
getCurrentConfig().model
);
useEffect(() => {
loadSettings();
}, []);
// Update image sizes when quality changes
useEffect(() => {
const config = getCurrentConfig();
const sizes = getImageSizes(config.service, config.model);
const defaultSize = sizes.length > 0 ? sizes[0].value : '1024x1024';
const validSizes = sizes.map(s => s.value);
const needsFeaturedUpdate = !validSizes.includes(imageSettings.featured_image_size);
const needsDesktopUpdate = !validSizes.includes(imageSettings.desktop_image_size);
if (needsFeaturedUpdate || needsDesktopUpdate) {
setImageSettings(prev => ({
...prev,
service: config.service,
provider: config.service,
model: config.model,
featured_image_size: needsFeaturedUpdate ? defaultSize : prev.featured_image_size,
desktop_image_size: needsDesktopUpdate ? defaultSize : prev.desktop_image_size,
}));
} else {
setImageSettings(prev => ({
...prev,
service: config.service,
provider: config.service,
model: config.model,
}));
}
}, [imageQuality, getCurrentConfig]);
const loadSettings = async () => {
try {
setLoading(true);
// Load image generation settings
const imageData = await fetchAPI('/v1/system/settings/integrations/image_generation/');
if (imageData) {
const quality = getQualityFromConfig(imageData.service || imageData.provider, imageData.model);
setImageQuality(quality);
setImageSettings({
enabled: imageData.enabled !== false,
service: imageData.service || imageData.provider || 'openai',
provider: imageData.provider || imageData.service || 'openai',
model: imageData.model || 'dall-e-3',
runwareModel: imageData.runwareModel,
image_type: imageData.image_type || 'realistic',
max_in_article_images: imageData.max_in_article_images || 2,
image_format: imageData.image_format || 'webp',
desktop_enabled: imageData.desktop_enabled !== false,
mobile_enabled: imageData.mobile_enabled !== false,
featured_image_size: imageData.featured_image_size || '1024x1024',
desktop_image_size: imageData.desktop_image_size || '1024x1024',
});
}
// TODO: Load content generation settings when API is available
// TODO: Load publishing settings when API is available
} catch (error: any) {
console.error('Error loading content settings:', error);
} finally {
setLoading(false);
}
};
const handleSave = async () => {
try {
setSaving(true);
if (activeTab === 'images') {
const config = getCurrentConfig();
const configToSave = {
enabled: imageSettings.enabled,
service: config.service,
provider: config.service,
model: config.model,
runwareModel: config.service === 'runware' ? config.model : undefined,
image_type: imageSettings.image_type,
max_in_article_images: imageSettings.max_in_article_images,
image_format: imageSettings.image_format,
desktop_enabled: imageSettings.desktop_enabled,
mobile_enabled: imageSettings.mobile_enabled,
featured_image_size: imageSettings.featured_image_size,
desktop_image_size: imageSettings.desktop_image_size,
};
await fetchAPI('/v1/system/settings/integrations/image_generation/save/', {
method: 'POST',
body: JSON.stringify(configToSave),
});
}
// TODO: Save content generation settings when API is available
// TODO: Save publishing settings when API is available
toast.success('Settings saved successfully');
} catch (error: any) {
console.error('Error saving settings:', error);
toast.error(`Failed to save settings: ${error.message}`);
} finally {
setSaving(false);
}
};
const tabs = [
{ id: 'content' as TabType, label: 'Content Generation', icon: <FileText className="w-4 h-4" /> },
{ id: 'publishing' as TabType, label: 'Publishing', icon: <Send className="w-4 h-4" /> },
{ id: 'images' as TabType, label: 'Image Settings', icon: <ImageIcon className="w-4 h-4" /> },
];
if (loading) {
return (
<div className="p-6">
<PageMeta title="Content Settings" description="Configure your content generation settings" />
<div className="flex items-center justify-center h-64">
<div className="flex flex-col items-center gap-3">
<Loader2 className="w-8 h-8 animate-spin text-[var(--color-brand-500)]" />
<div className="text-gray-500 dark:text-gray-400">Loading settings...</div>
</div>
</div>
</div>
);
}
return (
<div className="p-6">
<PageMeta title="Content Settings" description="Configure your content generation settings" />
{/* Page Header */}
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Content Settings</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Configure how your content and images are generated
</p>
</div>
{/* Tabs */}
<div className="mb-6 border-b border-gray-200 dark:border-gray-700">
<nav className="-mb-px flex space-x-8">
{tabs.map((tab) => (
<button
type="button"
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`
flex items-center gap-2 py-4 px-1 border-b-2 font-medium text-sm
${activeTab === tab.id
? 'border-[var(--color-brand-500)] text-[var(--color-brand-600)] dark:text-[var(--color-brand-400)]'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
}
`}
>
{tab.icon}
{tab.label}
</button>
))}
</nav>
</div>
{/* Tab Content */}
<div className="mt-6">
{/* Content Generation Tab */}
{activeTab === 'content' && (
<div className="space-y-6 max-w-4xl">
<Card className="p-6">
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-blue-100 dark:bg-blue-900/30 rounded-lg">
<FileText className="w-5 h-5 text-blue-600 dark:text-blue-400" />
</div>
<div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Content Generation</h2>
<p className="text-sm text-gray-500 dark:text-gray-400">Customize how your articles are written</p>
</div>
</div>
<div className="space-y-6">
<div>
<Label className="mb-2">Append to Every Prompt</Label>
<textarea
value={contentSettings.appendToPrompt}
onChange={(e) => setContentSettings({ ...contentSettings, appendToPrompt: e.target.value })}
placeholder="Add custom instructions that will be included with every content generation request..."
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-[var(--color-brand-500)] dark:bg-gray-800 min-h-[120px] resize-y"
/>
<p className="text-xs text-gray-500 mt-1">
This text will be appended to every AI prompt. Use it to enforce brand guidelines, tone, or specific requirements.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<Label className="mb-2">Default Writing Tone</Label>
<SelectDropdown
options={[
{ value: 'professional', label: 'Professional' },
{ value: 'conversational', label: 'Conversational' },
{ value: 'formal', label: 'Formal' },
{ value: 'casual', label: 'Casual' },
{ value: 'friendly', label: 'Friendly' },
]}
value={contentSettings.defaultTone}
onChange={(value) => setContentSettings({ ...contentSettings, defaultTone: value })}
className="w-full"
/>
</div>
<div>
<Label className="mb-2">Default Article Length</Label>
<SelectDropdown
options={[
{ value: 'short', label: 'Short (500-800 words)' },
{ value: 'medium', label: 'Medium (1000-1500 words)' },
{ value: 'long', label: 'Long (2000-3000 words)' },
{ value: 'comprehensive', label: 'Comprehensive (3000+ words)' },
]}
value={contentSettings.defaultLength}
onChange={(value) => setContentSettings({ ...contentSettings, defaultLength: value })}
className="w-full"
/>
</div>
</div>
</div>
</Card>
{/* Save Button */}
<div className="flex justify-end">
<Button
variant="primary"
tone="brand"
onClick={handleSave}
disabled={saving}
startIcon={saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
>
{saving ? 'Saving...' : 'Save Settings'}
</Button>
</div>
</div>
)}
{/* Publishing Tab */}
{activeTab === 'publishing' && (
<div className="space-y-6 max-w-4xl">
<Card className="p-6">
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-green-100 dark:bg-green-900/30 rounded-lg">
<Send className="w-5 h-5 text-green-600 dark:text-green-400" />
</div>
<div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">WordPress Publishing</h2>
<p className="text-sm text-gray-500 dark:text-gray-400">Configure automatic publishing to your WordPress sites</p>
</div>
</div>
<div className="space-y-6">
{/* Auto-Publish Setting */}
<div className="p-4 rounded-lg border border-gray-200 dark:border-gray-700">
<div className="flex items-start gap-3">
<Checkbox
checked={publishingSettings.autoPublishEnabled}
onChange={(checked) => setPublishingSettings({ ...publishingSettings, autoPublishEnabled: checked })}
/>
<div className="flex-1">
<Label className="font-medium text-gray-900 dark:text-white">
Automatic Publishing
</Label>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
Automatically publish articles to WordPress when they're finished and reviewed
</p>
</div>
</div>
{publishingSettings.autoPublishEnabled && (
<div className="mt-4 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
<p className="text-sm text-blue-800 dark:text-blue-200">
Articles will be published automatically once they pass review. You can still manually review them first if needed.
</p>
</div>
)}
</div>
{/* Auto-Sync Setting */}
<div className="p-4 rounded-lg border border-gray-200 dark:border-gray-700">
<div className="flex items-start gap-3">
<Checkbox
checked={publishingSettings.autoSyncEnabled}
onChange={(checked) => setPublishingSettings({ ...publishingSettings, autoSyncEnabled: checked })}
/>
<div className="flex-1">
<Label className="font-medium text-gray-900 dark:text-white">
Keep Content Updated
</Label>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
Automatically update articles on WordPress if you make changes here
</p>
</div>
</div>
</div>
</div>
</Card>
{/* Save Button */}
<div className="flex justify-end">
<Button
variant="primary"
tone="brand"
onClick={handleSave}
disabled={saving}
startIcon={saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
>
{saving ? 'Saving...' : 'Save Settings'}
</Button>
</div>
</div>
)}
{/* Image Settings Tab */}
{activeTab === 'images' && (
<div className="space-y-6 max-w-4xl">
<Card className="p-6">
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-purple-100 dark:bg-purple-900/30 rounded-lg">
<ImageIcon className="w-5 h-5 text-purple-600 dark:text-purple-400" />
</div>
<div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">Image Generation</h2>
<p className="text-sm text-gray-500 dark:text-gray-400">Configure how images are created for your articles</p>
</div>
</div>
<div className="space-y-6">
{/* Row 1: Image Quality & Style */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<Label className="mb-2">Image Quality</Label>
<SelectDropdown
options={[
{ value: 'standard', label: 'Standard - Fast & economical (DALL·E 2)' },
{ value: 'premium', label: 'Premium - High quality (DALL·E 3)' },
{ value: 'best', label: 'Best - Highest quality (Runware)' },
]}
value={imageQuality}
onChange={(value) => setImageQuality(value as 'standard' | 'premium' | 'best')}
className="w-full"
/>
<p className="text-xs text-gray-500 mt-1">
Higher quality produces better images
</p>
</div>
<div>
<Label className="mb-2">Image Style</Label>
<SelectDropdown
options={[
{ value: 'realistic', label: 'Realistic' },
{ value: 'artistic', label: 'Artistic' },
{ value: 'cartoon', label: 'Cartoon' },
]}
value={imageSettings.image_type}
onChange={(value) => setImageSettings({ ...imageSettings, image_type: value as any })}
className="w-full"
/>
<p className="text-xs text-gray-500 mt-1">
Choose the visual style that matches your brand
</p>
</div>
</div>
{/* Row 2: Featured Image Size */}
<div>
<Label className="mb-2">Featured Image</Label>
<div className="p-4 rounded-lg border border-gray-200 dark:border-gray-700 bg-gradient-to-r from-purple-500 to-blue-500 text-white">
<div className="flex items-center justify-between mb-3">
<div className="font-medium">Featured Image Size</div>
<div className="text-xs bg-white/20 px-2 py-1 rounded">Always Enabled</div>
</div>
<SelectDropdown
options={availableSizes}
value={imageSettings.featured_image_size}
onChange={(value) => setImageSettings({ ...imageSettings, featured_image_size: value })}
className="w-full [&_.igny8-select-styled]:bg-white/10 [&_.igny8-select-styled]:border-white/20 [&_.igny8-select-styled]:text-white"
/>
</div>
</div>
{/* Row 3: Desktop & Mobile Images */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="p-4 rounded-lg border border-gray-200 dark:border-gray-700 space-y-3">
<div className="flex items-center gap-3">
<Checkbox
checked={imageSettings.desktop_enabled}
onChange={(checked) => setImageSettings({ ...imageSettings, desktop_enabled: checked })}
/>
<Label className="font-medium text-gray-700 dark:text-gray-300">
Desktop Images
</Label>
</div>
{imageSettings.desktop_enabled && (
<SelectDropdown
options={availableSizes}
value={imageSettings.desktop_image_size}
onChange={(value) => setImageSettings({ ...imageSettings, desktop_image_size: value })}
className="w-full"
/>
)}
</div>
<div className="flex items-center gap-3 p-4 rounded-lg border border-gray-200 dark:border-gray-700">
<Checkbox
checked={imageSettings.mobile_enabled}
onChange={(checked) => setImageSettings({ ...imageSettings, mobile_enabled: checked })}
/>
<div>
<Label className="font-medium text-gray-700 dark:text-gray-300">
Mobile Images
</Label>
<div className="text-xs text-gray-500 dark:text-gray-400">
512×512 pixels
</div>
</div>
</div>
</div>
{/* Row 4: Max Images & Format */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<Label className="mb-2">Max In-Article Images</Label>
<SelectDropdown
options={[
{ value: '1', label: '1 Image' },
{ value: '2', label: '2 Images' },
{ value: '3', label: '3 Images' },
{ value: '4', label: '4 Images' },
{ value: '5', label: '5 Images' },
]}
value={String(imageSettings.max_in_article_images)}
onChange={(value) => setImageSettings({ ...imageSettings, max_in_article_images: parseInt(value) })}
className="w-full"
/>
</div>
<div>
<Label className="mb-2">Image Format</Label>
<SelectDropdown
options={[
{ value: 'webp', label: 'WEBP (recommended)' },
{ value: 'jpg', label: 'JPG' },
{ value: 'png', label: 'PNG' },
]}
value={imageSettings.image_format}
onChange={(value) => setImageSettings({ ...imageSettings, image_format: value as any })}
className="w-full"
/>
</div>
</div>
</div>
</Card>
{/* Save Button */}
<div className="flex justify-end">
<Button
variant="primary"
tone="brand"
onClick={handleSave}
disabled={saving}
startIcon={saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
>
{saving ? 'Saving...' : 'Save Settings'}
</Button>
</div>
</div>
)}
</div>
</div>
);
}

View File

@@ -1,12 +1,15 @@
/**
* Plans & Billing Page - Refactored for Better UX
* Organized tabs: Current Plan, Plan Limits, Usage, Upgrade Plan, Billing History
* Plans & Billing Page - Subscription & Payment Management
* Tabs: Current Plan, Upgrade Plan, Billing History
*
* Note: Usage tracking is consolidated in UsageAnalyticsPage (/account/usage)
*/
import { useState, useEffect, useRef } from 'react';
import { Link } from 'react-router-dom';
import {
CreditCard, Package, TrendingUp, FileText, Wallet, ArrowUpCircle,
Loader2, AlertCircle, CheckCircle, Download, BarChart3, Zap, Globe, Users
Loader2, AlertCircle, CheckCircle, Download, Zap, Globe, Users
} from 'lucide-react';
import { Card } from '../../components/ui/card';
import Badge from '../../components/ui/badge/Badge';
@@ -16,7 +19,7 @@ import { PricingPlan } from '../../components/ui/pricing-table';
import PricingTable1 from '../../components/ui/pricing-table/pricing-table-1';
import CreditCostBreakdownPanel from '../../components/billing/CreditCostBreakdownPanel';
// import CreditCostsPanel from '../../components/billing/CreditCostsPanel'; // Hidden from regular users
import UsageLimitsPanel from '../../components/billing/UsageLimitsPanel';
// import UsageLimitsPanel from '../../components/billing/UsageLimitsPanel'; // Moved to UsageAnalyticsPage
import { convertToPricingPlan } from '../../utils/pricingHelpers';
import {
getCreditBalance,
@@ -44,7 +47,7 @@ import {
} from '../../services/billing.api';
import { useAuthStore } from '../../store/authStore';
type TabType = 'plan' | 'limits' | 'credits' | 'upgrade' | 'invoices';
type TabType = 'plan' | 'upgrade' | 'invoices';
export default function PlansAndBillingPage() {
const [activeTab, setActiveTab] = useState<TabType>('plan');
@@ -345,7 +348,6 @@ export default function PlansAndBillingPage() {
const tabs = [
{ id: 'plan' as TabType, label: 'Current Plan', icon: <Package className="w-4 h-4" /> },
{ id: 'limits' as TabType, label: 'Usage', icon: <BarChart3 className="w-4 h-4" /> },
{ id: 'upgrade' as TabType, label: 'Upgrade Plan', icon: <Wallet className="w-4 h-4" /> },
{ id: 'invoices' as TabType, label: 'History', icon: <FileText className="w-4 h-4" /> },
];
@@ -481,9 +483,10 @@ export default function PlansAndBillingPage() {
<Button
variant="outline"
tone="neutral"
onClick={() => setActiveTab('limits')}
as={Link}
to="/account/usage"
>
View Limits
View Usage
</Button>
{hasActivePlan && (
<Button
@@ -537,9 +540,10 @@ export default function PlansAndBillingPage() {
variant="outline"
tone="brand"
size="sm"
onClick={() => setActiveTab('limits')}
as={Link}
to="/account/usage"
>
View All Limits
View All Usage
</Button>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
@@ -589,109 +593,6 @@ export default function PlansAndBillingPage() {
</div>
)}
{/* Plan Limits Tab */}
{activeTab === 'limits' && (
<div className="space-y-6">
<UsageLimitsPanel />
</div>
)}
{/* Usage Overview Tab */}
{activeTab === 'credits' && (
<div className="space-y-6">
{/* Usage Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card className="p-6 bg-gradient-to-br from-brand-50 to-brand-100 dark:from-brand-900/20 dark:to-brand-800/10 border-brand-200 dark:border-brand-700">
<div className="flex items-center gap-3 mb-3">
<div className="p-2 bg-brand-500 rounded-lg">
<Wallet className="w-5 h-5 text-white" />
</div>
<div className="text-sm font-medium text-brand-700 dark:text-brand-300">Content Remaining</div>
</div>
<div className="text-4xl font-bold text-brand-600 dark:text-brand-400">
{creditBalance?.credits.toLocaleString() || 0}
</div>
<div className="text-sm text-brand-600 dark:text-brand-400 mt-2">pieces available</div>
</Card>
<Card className="p-6 bg-gradient-to-br from-red-50 to-red-100 dark:from-red-900/20 dark:to-red-800/10 border-red-200 dark:border-red-700">
<div className="flex items-center gap-3 mb-3">
<div className="p-2 bg-red-500 rounded-lg">
<TrendingUp className="w-5 h-5 text-white" />
</div>
<div className="text-sm font-medium text-red-700 dark:text-red-300">Used This Month</div>
</div>
<div className="text-4xl font-bold text-red-600 dark:text-red-400">
{creditBalance?.credits_used_this_month.toLocaleString() || 0}
</div>
<div className="text-sm text-red-600 dark:text-red-400 mt-2">credits consumed</div>
</Card>
<Card className="p-6 bg-gradient-to-br from-success-50 to-success-100 dark:from-success-900/20 dark:to-success-800/10 border-success-200 dark:border-success-700">
<div className="flex items-center gap-3 mb-3">
<div className="p-2 bg-success-500 rounded-lg">
<Package className="w-5 h-5 text-white" />
</div>
<div className="text-sm font-medium text-success-700 dark:text-success-300">Monthly Included</div>
</div>
<div className="text-4xl font-bold text-success-600 dark:text-success-400">
{creditBalance?.plan_credits_per_month.toLocaleString() || 0}
</div>
<div className="text-sm text-success-600 dark:text-success-400 mt-2">from your plan</div>
</Card>
</div>
{/* Usage Summary with Progress Bar */}
<Card className="p-6">
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Credit Usage Summary</h2>
<div className="space-y-4">
<div className="flex justify-between items-center text-sm">
<span className="text-gray-700 dark:text-gray-300">Monthly Allocation</span>
<span className="font-semibold text-gray-900 dark:text-white">
{creditBalance?.plan_credits_per_month.toLocaleString() || 0} credits
</span>
</div>
<div className="flex justify-between items-center text-sm">
<span className="text-gray-700 dark:text-gray-300">Used This Month</span>
<span className="font-semibold text-red-600 dark:text-red-400">
{creditBalance?.credits_used_this_month.toLocaleString() || 0} credits
</span>
</div>
<div className="flex justify-between items-center text-sm">
<span className="text-gray-700 dark:text-gray-300">Remaining Balance</span>
<span className="font-semibold text-success-600 dark:text-success-400">
{creditBalance?.credits_remaining.toLocaleString() || 0} credits
</span>
</div>
{/* Progress Bar */}
<div className="pt-2">
<div className="flex justify-between items-center text-xs text-gray-600 dark:text-gray-400 mb-2">
<span>Usage Progress</span>
<span>
{creditBalance?.credits_used_this_month && creditBalance?.plan_credits_per_month
? `${Math.round((creditBalance.credits_used_this_month / creditBalance.plan_credits_per_month) * 100)}%`
: '0%'}
</span>
</div>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-3">
<div
className="bg-gradient-to-r from-brand-500 to-brand-600 h-3 rounded-full transition-all duration-300"
style={{
width: creditBalance?.credits_used_this_month && creditBalance?.plan_credits_per_month
? `${Math.min((creditBalance.credits_used_this_month / creditBalance.plan_credits_per_month) * 100, 100)}%`
: '0%'
}}
></div>
</div>
</div>
</div>
</Card>
{/* Usage Analytics - removed detailed credit breakdown to simplify user view */}
</div>
)}
{/* Purchase/Upgrade Tab */}
{activeTab === 'upgrade' && (
<div className="space-y-6">
@@ -752,110 +653,7 @@ export default function PlansAndBillingPage() {
</Card>
</div>
{/* Purchase Additional Credits Section - Hidden from regular users
<div className="mt-12 pt-8 border-t-2 border-gray-200 dark:border-gray-700">
<div className="mb-6">
<h2 className="text-xl font-semibold mb-2 text-gray-900 dark:text-white">Purchase Additional Credits</h2>
<p className="text-gray-600 dark:text-gray-400">Top up your credit balance with our credit packages</p>
</div>
{/* Current Balance Quick View */}
<Card className="p-6 bg-gradient-to-r from-brand-50 to-purple-50 dark:from-brand-900/20 dark:to-purple-900/20 border-brand-200 dark:border-brand-700">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="p-3 bg-brand-500 rounded-lg">
<Wallet className="w-6 h-6 text-white" />
</div>
<div>
<div className="text-sm text-gray-600 dark:text-gray-400">Content Remaining</div>
<div className="text-3xl font-bold text-gray-900 dark:text-white">
{creditBalance?.credits.toLocaleString() || 0}
</div>
</div>
</div>
<div className="text-right">
<div className="text-sm text-gray-600 dark:text-gray-400">Monthly Allowance</div>
<div className="text-xl font-bold text-brand-600 dark:text-brand-400">
{creditBalance?.plan_credits_per_month.toLocaleString() || 0}
</div>
</div>
</div>
</Card>
{/* Credit Packages Grid - Hidden from regular users */}
{/*
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mt-6">
{packages.map((pkg) => (
<Card
key={pkg.id}
className="p-6 hover:shadow-lg transition-all duration-200 hover:border-brand-300 dark:hover:border-brand-600"
>
<div className="mb-4">
<div className="inline-flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br from-brand-500 to-brand-600 shadow-md">
<Zap className="w-6 h-6 text-white" />
</div>
</div>
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">
{pkg.name}
</h3>
{pkg.description && (
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
{pkg.description}
</p>
)}
<div className="flex items-baseline gap-2 mb-1">
<span className="text-4xl font-bold text-brand-600 dark:text-brand-400">
{pkg.credits.toLocaleString()}
</span>
<span className="text-sm text-gray-500 dark:text-gray-400">credits</span>
</div>
<div className="text-2xl font-semibold text-gray-900 dark:text-white mb-6">
${pkg.price}
</div>
<Button
variant="primary"
tone="brand"
onClick={() => handlePurchase(pkg.id)}
fullWidth
size="md"
disabled={purchaseLoadingId === pkg.id || (!hasPaymentMethods && paymentMethods.length > 0)}
startIcon={purchaseLoadingId === pkg.id ? <Loader2 className="w-4 h-4 animate-spin" /> : undefined}
>
{purchaseLoadingId === pkg.id ? 'Processing...' : 'Purchase Now'}
</Button>
</Card>
))}
{packages.length === 0 && (
<div className="col-span-3 text-center py-16">
<div className="inline-flex h-16 w-16 items-center justify-center rounded-full bg-gray-100 dark:bg-gray-800 mb-4">
<Package className="w-8 h-8 text-gray-400" />
</div>
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">No Packages Available</h3>
<p className="text-gray-500 dark:text-gray-400">Credit packages will be available soon</p>
</div>
)}
</div>
{/* Payment Methods Info */}
{!hasPaymentMethods && paymentMethods.length === 0 && (
<Card className="p-6 bg-warning-50 dark:bg-warning-900/20 border-warning-200 dark:border-warning-700 mt-6">
<div className="flex items-start gap-3">
<div className="p-2 bg-warning-100 dark:bg-warning-800/50 rounded-lg">
<AlertCircle className="w-5 h-5 text-warning-600 dark:text-warning-400" />
</div>
<div>
<h3 className="font-semibold text-warning-900 dark:text-warning-100 mb-1">
Payment Method Required
</h3>
<p className="text-sm text-warning-800 dark:text-warning-200">
Please contact support to set up a payment method before purchasing credits.
</p>
</div>
</div>
</Card>
)}
</div>
*/}
{/* Purchase Additional Credits Section - Hidden from regular users - removed for simplification */}
</div>
)}