diff --git a/frontend/src/api/backlinkOutreachApi.ts b/frontend/src/api/backlinkOutreachApi.ts index a2d2378b..b9fddf93 100644 --- a/frontend/src/api/backlinkOutreachApi.ts +++ b/frontend/src/api/backlinkOutreachApi.ts @@ -71,6 +71,29 @@ export interface BacklinkDiscoveryResponse { opportunities: BacklinkOpportunity[]; } +export interface BacklinkCampaignRecord { + campaign_id: string; + name: string; + status: string; + created_at?: string; +} + +export interface BacklinkCampaignCreateRequest { + user_id: string; + workspace_id: string; + name: string; +} + +export interface BacklinkCampaignCreateResponse { + campaign_id: string; + name: string; + status: string; +} + +export interface BacklinkCampaignListResponse { + campaigns: BacklinkCampaignRecord[]; +} + export const fetchBacklinkModuleRegistry = async (): Promise => (await apiClient.get('/api/backlink-outreach/modules')).data; export const fetchBacklinkMigrationCoverage = async (): Promise => (await apiClient.get('/api/backlink-outreach/migration-coverage')).data; export const fetchBacklinkQueryTemplates = async (keyword: string): Promise => (await apiClient.get('/api/backlink-outreach/query-templates', { params: { keyword } })).data; @@ -78,3 +101,6 @@ export const discoverBacklinkOpportunities = async (payload: BacklinkDiscoveryRe export const validateBacklinkPolicy = async (payload: BacklinkPolicyValidationRequest): Promise => (await apiClient.post('/api/backlink-outreach/policy-validate', payload)).data; export const fetchBacklinkReportingSnapshot = async (): Promise => (await apiClient.get('/api/backlink-outreach/reporting')).data; + +export const createBacklinkCampaign = async (payload: BacklinkCampaignCreateRequest): Promise => (await apiClient.post('/api/backlink-outreach/campaigns', payload)).data; +export const listBacklinkCampaigns = async (user_id: string, workspace_id: string): Promise => (await apiClient.get('/api/backlink-outreach/campaigns', { params: { user_id, workspace_id } })).data; diff --git a/frontend/src/components/SEODashboard/BacklinkOutreachModuleList.tsx b/frontend/src/components/SEODashboard/BacklinkOutreachModuleList.tsx index 865645e2..4fb49cfe 100644 --- a/frontend/src/components/SEODashboard/BacklinkOutreachModuleList.tsx +++ b/frontend/src/components/SEODashboard/BacklinkOutreachModuleList.tsx @@ -1,11 +1,12 @@ -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { fetchBacklinkQueryTemplates } from '../../api/backlinkOutreachApi'; import { useBacklinkOutreachStore } from '../../stores/backlinkOutreachStore'; const BacklinkOutreachModuleList: React.FC = () => { - const { modules, coverage, isLoading, error, refreshBacklinkRegistry } = useBacklinkOutreachStore(); + const { modules, coverage, campaigns, isLoading, error, refreshBacklinkRegistry, fetchCampaigns, createCampaign } = useBacklinkOutreachStore(); const [queryPreview, setQueryPreview] = useState([]); + const [newCampaignName, setNewCampaignName] = useState(''); useEffect(() => { refreshBacklinkRegistry(); @@ -19,6 +20,16 @@ const BacklinkOutreachModuleList: React.FC = () => { loadPreview().catch(() => setQueryPreview([])); }, []); + useEffect(() => { + fetchCampaigns('default', 'default').catch(() => {}); + }, [fetchCampaigns]); + + const handleCreateCampaign = useCallback(async () => { + if (!newCampaignName.trim()) return; + await createCampaign('default', 'default', newCampaignName.trim()); + setNewCampaignName(''); + }, [newCampaignName, createCampaign]); + return (

Backlink Outreach Modules

@@ -51,6 +62,31 @@ const BacklinkOutreachModuleList: React.FC = () => { )} + +

Campaigns

+ {campaigns.length === 0 ? ( +

No campaigns yet. Create one below.

+ ) : ( +
    + {campaigns.map((c) => ( +
  • + {c.name} ({c.status}) + {c.created_at && — {new Date(c.created_at).toLocaleDateString()}} +
  • + ))} +
+ )} +
+ setNewCampaignName(e.target.value)} + placeholder="Campaign name" + /> + +
)}
diff --git a/frontend/src/stores/backlinkOutreachStore.ts b/frontend/src/stores/backlinkOutreachStore.ts index e6b83521..b9a01fbc 100644 --- a/frontend/src/stores/backlinkOutreachStore.ts +++ b/frontend/src/stores/backlinkOutreachStore.ts @@ -1,23 +1,30 @@ import { create } from 'zustand'; import { + BacklinkCampaignRecord, BacklinkCoverageResponse, BacklinkModuleRecord, + createBacklinkCampaign, fetchBacklinkMigrationCoverage, fetchBacklinkModuleRegistry, + listBacklinkCampaigns, } from '../api/backlinkOutreachApi'; interface BacklinkOutreachStore { modules: BacklinkModuleRecord[]; coverage: BacklinkCoverageResponse | null; + campaigns: BacklinkCampaignRecord[]; isLoading: boolean; error: string | null; refreshBacklinkRegistry: () => Promise; + fetchCampaigns: (userId: string, workspaceId: string) => Promise; + createCampaign: (userId: string, workspaceId: string, name: string) => Promise; } export const useBacklinkOutreachStore = create((set) => ({ modules: [], coverage: null, + campaigns: [], isLoading: false, error: null, refreshBacklinkRegistry: async () => { @@ -35,4 +42,33 @@ export const useBacklinkOutreachStore = create((set) => ( }); } }, + fetchCampaigns: async (userId: string, workspaceId: string) => { + set({ isLoading: true, error: null }); + try { + const response = await listBacklinkCampaigns(userId, workspaceId); + set({ campaigns: response.campaigns, isLoading: false }); + } catch (error: any) { + set({ + isLoading: false, + error: error?.message ?? 'Failed to load campaigns', + }); + } + }, + createCampaign: async (userId: string, workspaceId: string, name: string) => { + set({ isLoading: true, error: null }); + try { + const result = await createBacklinkCampaign({ user_id: userId, workspace_id: workspaceId, name }); + set((state) => ({ + campaigns: [...state.campaigns, { campaign_id: result.campaign_id, name: result.name, status: result.status }], + isLoading: false, + })); + return result.campaign_id; + } catch (error: any) { + set({ + isLoading: false, + error: error?.message ?? 'Failed to create campaign', + }); + return null; + } + }, }));