376 lines
8.6 KiB
Markdown
376 lines
8.6 KiB
Markdown
# Zustand State Management
|
|
|
|
**Last Verified:** December 25, 2025
|
|
**Framework:** Zustand 4 with persist middleware
|
|
|
|
---
|
|
|
|
## Store Architecture
|
|
|
|
All stores in `/frontend/src/store/` use Zustand with TypeScript.
|
|
|
|
**Key Patterns:**
|
|
- `persist` middleware for localStorage persistence
|
|
- Async actions for API calls
|
|
- Selectors for derived state
|
|
|
|
---
|
|
|
|
## Auth Store (`authStore.ts`)
|
|
|
|
**Purpose:** User authentication and session management
|
|
|
|
```typescript
|
|
interface AuthState {
|
|
user: User | null;
|
|
account: Account | null;
|
|
isAuthenticated: boolean;
|
|
accessToken: string | null;
|
|
refreshToken: string | null;
|
|
isLoading: boolean;
|
|
error: string | null;
|
|
}
|
|
|
|
interface AuthActions {
|
|
login(email: string, password: string): Promise<void>;
|
|
logout(): void;
|
|
register(data: RegisterData): Promise<void>;
|
|
refreshAccessToken(): Promise<void>;
|
|
fetchUser(): Promise<void>;
|
|
updateUser(data: UserUpdate): Promise<void>;
|
|
}
|
|
```
|
|
|
|
**Persistence:** `accessToken`, `refreshToken` in localStorage
|
|
|
|
**Usage:**
|
|
```typescript
|
|
const { user, login, logout, isAuthenticated } = useAuthStore();
|
|
```
|
|
|
|
---
|
|
|
|
## Site Store (`siteStore.ts`)
|
|
|
|
**Purpose:** Site selection and management
|
|
|
|
```typescript
|
|
interface SiteState {
|
|
sites: Site[];
|
|
currentSite: Site | null;
|
|
isLoading: boolean;
|
|
error: string | null;
|
|
}
|
|
|
|
interface SiteActions {
|
|
fetchSites(): Promise<void>;
|
|
createSite(data: SiteCreate): Promise<Site>;
|
|
updateSite(id: string, data: SiteUpdate): Promise<Site>;
|
|
deleteSite(id: string): Promise<void>;
|
|
setCurrentSite(site: Site): void;
|
|
}
|
|
```
|
|
|
|
**Persistence:** `currentSite.id` in localStorage
|
|
|
|
**Auto-selection:** If no site selected and sites exist, auto-selects first site
|
|
|
|
---
|
|
|
|
## Sector Store (`sectorStore.ts`)
|
|
|
|
**Purpose:** Sector selection and management within sites
|
|
|
|
```typescript
|
|
interface SectorState {
|
|
sectors: Sector[];
|
|
currentSector: Sector | null;
|
|
isLoading: boolean;
|
|
error: string | null;
|
|
}
|
|
|
|
interface SectorActions {
|
|
fetchSectors(siteId: string): Promise<void>;
|
|
createSector(data: SectorCreate): Promise<Sector>;
|
|
updateSector(id: string, data: SectorUpdate): Promise<Sector>;
|
|
deleteSector(id: string): Promise<void>;
|
|
setCurrentSector(sector: Sector): void;
|
|
}
|
|
```
|
|
|
|
**Persistence:** `currentSector.id` in localStorage
|
|
|
|
**Dependency:** Reloads when `currentSite` changes
|
|
|
|
---
|
|
|
|
## Module Store (`moduleStore.ts`)
|
|
|
|
**Purpose:** Track which modules are enabled/disabled
|
|
|
|
```typescript
|
|
interface ModuleState {
|
|
modules: ModuleSettings;
|
|
isLoading: boolean;
|
|
error: string | null;
|
|
}
|
|
|
|
interface ModuleSettings {
|
|
planner_enabled: boolean;
|
|
writer_enabled: boolean;
|
|
linker_enabled: boolean;
|
|
optimizer_enabled: boolean;
|
|
automation_enabled: boolean;
|
|
integration_enabled: boolean;
|
|
publisher_enabled: boolean;
|
|
}
|
|
|
|
interface ModuleActions {
|
|
fetchModules(): Promise<void>;
|
|
updateModules(settings: Partial<ModuleSettings>): Promise<void>;
|
|
isModuleEnabled(module: ModuleName): boolean;
|
|
}
|
|
```
|
|
|
|
**Usage:**
|
|
```typescript
|
|
const { isModuleEnabled } = useModuleStore();
|
|
if (isModuleEnabled('planner')) { /* show planner */ }
|
|
```
|
|
|
|
**Currently used for:** Sidebar visibility only
|
|
|
|
---
|
|
|
|
## Billing Store (`billingStore.ts`)
|
|
|
|
**Purpose:** Credit balance and usage tracking
|
|
|
|
```typescript
|
|
interface BillingState {
|
|
balance: CreditBalance | null;
|
|
usage: CreditUsage[];
|
|
limits: PlanLimits | null;
|
|
isLoading: boolean;
|
|
error: string | null;
|
|
}
|
|
|
|
interface CreditBalance {
|
|
ideaCredits: number;
|
|
contentCredits: number;
|
|
imageCredits: number;
|
|
optimizationCredits: number;
|
|
}
|
|
|
|
interface BillingActions {
|
|
fetchBalance(): Promise<void>;
|
|
fetchUsage(period?: string): Promise<void>;
|
|
fetchLimits(): Promise<void>;
|
|
}
|
|
```
|
|
|
|
**Refresh triggers:**
|
|
- After content generation
|
|
- After image generation
|
|
- After optimization (when implemented)
|
|
|
|
---
|
|
|
|
## Planner Store (`plannerStore.ts`)
|
|
|
|
**Purpose:** Keywords, clusters, and content ideas state
|
|
|
|
```typescript
|
|
interface PlannerState {
|
|
keywords: Keyword[];
|
|
clusters: Cluster[];
|
|
ideas: ContentIdea[];
|
|
selectedKeywords: string[];
|
|
filters: KeywordFilters;
|
|
isLoading: boolean;
|
|
error: string | null;
|
|
}
|
|
|
|
interface PlannerActions {
|
|
fetchKeywords(siteId: string, sectorId?: string): Promise<void>;
|
|
createKeyword(data: KeywordCreate): Promise<Keyword>;
|
|
bulkDeleteKeywords(ids: string[]): Promise<void>;
|
|
autoCluster(keywordIds: string[]): Promise<void>;
|
|
generateIdeas(clusterId: string): Promise<void>;
|
|
setFilters(filters: Partial<KeywordFilters>): void;
|
|
selectKeywords(ids: string[]): void;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Writer Store (`writerStore.ts`)
|
|
|
|
**Purpose:** Tasks and content management
|
|
|
|
```typescript
|
|
interface WriterState {
|
|
tasks: Task[];
|
|
content: Content[];
|
|
currentContent: Content | null;
|
|
filters: TaskFilters;
|
|
isLoading: boolean;
|
|
isGenerating: boolean;
|
|
error: string | null;
|
|
}
|
|
|
|
interface WriterActions {
|
|
fetchTasks(siteId: string, sectorId?: string): Promise<void>;
|
|
createTask(data: TaskCreate): Promise<Task>;
|
|
generateContent(taskId: string): Promise<Content>;
|
|
fetchContent(contentId: string): Promise<Content>;
|
|
updateContent(id: string, data: ContentUpdate): Promise<Content>;
|
|
generateImages(contentId: string): Promise<void>;
|
|
publishToWordPress(contentId: string): Promise<void>;
|
|
}
|
|
```
|
|
|
|
**Generation state:** `isGenerating` tracks active AI operations
|
|
|
|
---
|
|
|
|
## Automation Store (`automationStore.ts`)
|
|
|
|
**Purpose:** Automation pipeline state and control
|
|
|
|
```typescript
|
|
interface AutomationState {
|
|
config: AutomationConfig | null;
|
|
currentRun: AutomationRun | null;
|
|
pipeline: PipelineOverview | null;
|
|
history: AutomationRun[];
|
|
logs: AutomationLog[];
|
|
isLoading: boolean;
|
|
error: string | null;
|
|
}
|
|
|
|
interface AutomationActions {
|
|
fetchConfig(siteId: string): Promise<void>;
|
|
updateConfig(data: ConfigUpdate): Promise<void>;
|
|
startRun(siteId: string): Promise<void>;
|
|
pauseRun(runId: string): Promise<void>;
|
|
resumeRun(runId: string): Promise<void>;
|
|
cancelRun(runId: string): Promise<void>;
|
|
fetchPipeline(siteId: string): Promise<void>;
|
|
fetchLogs(runId: string): Promise<void>;
|
|
}
|
|
```
|
|
|
|
**Polling:** Active runs trigger status polling every 5 seconds
|
|
|
|
---
|
|
|
|
## Integration Store (`integrationStore.ts`)
|
|
|
|
**Purpose:** WordPress integration management
|
|
|
|
```typescript
|
|
interface IntegrationState {
|
|
integrations: SiteIntegration[];
|
|
currentIntegration: SiteIntegration | null;
|
|
syncStatus: SyncStatus | null;
|
|
isLoading: boolean;
|
|
isSyncing: boolean;
|
|
error: string | null;
|
|
}
|
|
|
|
interface IntegrationActions {
|
|
fetchIntegrations(siteId: string): Promise<void>;
|
|
createIntegration(data: IntegrationCreate): Promise<SiteIntegration>;
|
|
testConnection(id: string): Promise<TestResult>;
|
|
triggerSync(id: string): Promise<void>;
|
|
fetchSyncStatus(id: string): Promise<SyncStatus>;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## UI Store (`uiStore.ts`)
|
|
|
|
**Purpose:** UI state (sidebar, modals, notifications)
|
|
|
|
```typescript
|
|
interface UIState {
|
|
sidebarOpen: boolean;
|
|
sidebarCollapsed: boolean;
|
|
theme: 'light' | 'dark' | 'system';
|
|
notifications: Notification[];
|
|
}
|
|
|
|
interface UIActions {
|
|
toggleSidebar(): void;
|
|
collapseSidebar(): void;
|
|
setTheme(theme: Theme): void;
|
|
addNotification(notification: Notification): void;
|
|
removeNotification(id: string): void;
|
|
}
|
|
```
|
|
|
|
**Persistence:** `theme`, `sidebarCollapsed` in localStorage
|
|
|
|
---
|
|
|
|
## Store Dependencies
|
|
|
|
```
|
|
authStore
|
|
│
|
|
└── siteStore (loads after auth)
|
|
│
|
|
└── sectorStore (loads when site changes)
|
|
│
|
|
├── plannerStore (scoped to site/sector)
|
|
├── writerStore (scoped to site/sector)
|
|
├── automationStore (scoped to site)
|
|
└── integrationStore (scoped to site)
|
|
|
|
moduleStore (global, loads once per account)
|
|
billingStore (global, loads once per account)
|
|
uiStore (local only, no API)
|
|
```
|
|
|
|
---
|
|
|
|
## Store Files Location
|
|
|
|
```
|
|
frontend/src/store/
|
|
├── authStore.ts
|
|
├── siteStore.ts
|
|
├── sectorStore.ts
|
|
├── moduleStore.ts
|
|
├── billingStore.ts
|
|
├── plannerStore.ts
|
|
├── writerStore.ts
|
|
├── automationStore.ts
|
|
├── integrationStore.ts
|
|
├── uiStore.ts
|
|
└── index.ts (re-exports)
|
|
```
|
|
|
|
---
|
|
|
|
## Best Practices
|
|
|
|
1. **Always scope to site/sector** when fetching module data
|
|
2. **Check module enabled** before showing features
|
|
3. **Handle loading states** in UI components
|
|
4. **Clear store on logout** to prevent data leaks
|
|
5. **Use selectors** for derived/filtered data
|
|
|
|
---
|
|
|
|
## Planned Changes
|
|
|
|
| Item | Description | Priority |
|
|
|------|-------------|----------|
|
|
| Add `linkerStore` | State for internal linking results | Medium |
|
|
| Add `optimizerStore` | State for optimization results | Medium |
|
|
| Better error typing | Typed error codes per store | Low |
|
|
| DevTools integration | Zustand devtools middleware | Low |
|