Add frontend campaign create/list to backlinkOutreachApi + store + component
This commit is contained in:
@@ -71,6 +71,29 @@ export interface BacklinkDiscoveryResponse {
|
|||||||
opportunities: BacklinkOpportunity[];
|
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<BacklinkModuleRegistryResponse> => (await apiClient.get('/api/backlink-outreach/modules')).data;
|
export const fetchBacklinkModuleRegistry = async (): Promise<BacklinkModuleRegistryResponse> => (await apiClient.get('/api/backlink-outreach/modules')).data;
|
||||||
export const fetchBacklinkMigrationCoverage = async (): Promise<BacklinkCoverageResponse> => (await apiClient.get('/api/backlink-outreach/migration-coverage')).data;
|
export const fetchBacklinkMigrationCoverage = async (): Promise<BacklinkCoverageResponse> => (await apiClient.get('/api/backlink-outreach/migration-coverage')).data;
|
||||||
export const fetchBacklinkQueryTemplates = async (keyword: string): Promise<BacklinkQueryTemplatesResponse> => (await apiClient.get('/api/backlink-outreach/query-templates', { params: { keyword } })).data;
|
export const fetchBacklinkQueryTemplates = async (keyword: string): Promise<BacklinkQueryTemplatesResponse> => (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<BacklinkPolicyValidationResponse> => (await apiClient.post('/api/backlink-outreach/policy-validate', payload)).data;
|
export const validateBacklinkPolicy = async (payload: BacklinkPolicyValidationRequest): Promise<BacklinkPolicyValidationResponse> => (await apiClient.post('/api/backlink-outreach/policy-validate', payload)).data;
|
||||||
export const fetchBacklinkReportingSnapshot = async (): Promise<BacklinkReportingSnapshot> => (await apiClient.get('/api/backlink-outreach/reporting')).data;
|
export const fetchBacklinkReportingSnapshot = async (): Promise<BacklinkReportingSnapshot> => (await apiClient.get('/api/backlink-outreach/reporting')).data;
|
||||||
|
|
||||||
|
export const createBacklinkCampaign = async (payload: BacklinkCampaignCreateRequest): Promise<BacklinkCampaignCreateResponse> => (await apiClient.post('/api/backlink-outreach/campaigns', payload)).data;
|
||||||
|
export const listBacklinkCampaigns = async (user_id: string, workspace_id: string): Promise<BacklinkCampaignListResponse> => (await apiClient.get('/api/backlink-outreach/campaigns', { params: { user_id, workspace_id } })).data;
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { fetchBacklinkQueryTemplates } from '../../api/backlinkOutreachApi';
|
import { fetchBacklinkQueryTemplates } from '../../api/backlinkOutreachApi';
|
||||||
import { useBacklinkOutreachStore } from '../../stores/backlinkOutreachStore';
|
import { useBacklinkOutreachStore } from '../../stores/backlinkOutreachStore';
|
||||||
|
|
||||||
const BacklinkOutreachModuleList: React.FC = () => {
|
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<string[]>([]);
|
const [queryPreview, setQueryPreview] = useState<string[]>([]);
|
||||||
|
const [newCampaignName, setNewCampaignName] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refreshBacklinkRegistry();
|
refreshBacklinkRegistry();
|
||||||
@@ -19,6 +20,16 @@ const BacklinkOutreachModuleList: React.FC = () => {
|
|||||||
loadPreview().catch(() => setQueryPreview([]));
|
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 (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<h3>Backlink Outreach Modules</h3>
|
<h3>Backlink Outreach Modules</h3>
|
||||||
@@ -51,6 +62,31 @@ const BacklinkOutreachModuleList: React.FC = () => {
|
|||||||
</ul>
|
</ul>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<h4>Campaigns</h4>
|
||||||
|
{campaigns.length === 0 ? (
|
||||||
|
<p>No campaigns yet. Create one below.</p>
|
||||||
|
) : (
|
||||||
|
<ul>
|
||||||
|
{campaigns.map((c) => (
|
||||||
|
<li key={c.campaign_id}>
|
||||||
|
<strong>{c.name}</strong> ({c.status})
|
||||||
|
{c.created_at && <span> — {new Date(c.created_at).toLocaleDateString()}</span>}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newCampaignName}
|
||||||
|
onChange={(e) => setNewCampaignName(e.target.value)}
|
||||||
|
placeholder="Campaign name"
|
||||||
|
/>
|
||||||
|
<button onClick={handleCreateCampaign} disabled={!newCampaignName.trim()}>
|
||||||
|
Create Campaign
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,23 +1,30 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
BacklinkCampaignRecord,
|
||||||
BacklinkCoverageResponse,
|
BacklinkCoverageResponse,
|
||||||
BacklinkModuleRecord,
|
BacklinkModuleRecord,
|
||||||
|
createBacklinkCampaign,
|
||||||
fetchBacklinkMigrationCoverage,
|
fetchBacklinkMigrationCoverage,
|
||||||
fetchBacklinkModuleRegistry,
|
fetchBacklinkModuleRegistry,
|
||||||
|
listBacklinkCampaigns,
|
||||||
} from '../api/backlinkOutreachApi';
|
} from '../api/backlinkOutreachApi';
|
||||||
|
|
||||||
interface BacklinkOutreachStore {
|
interface BacklinkOutreachStore {
|
||||||
modules: BacklinkModuleRecord[];
|
modules: BacklinkModuleRecord[];
|
||||||
coverage: BacklinkCoverageResponse | null;
|
coverage: BacklinkCoverageResponse | null;
|
||||||
|
campaigns: BacklinkCampaignRecord[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
refreshBacklinkRegistry: () => Promise<void>;
|
refreshBacklinkRegistry: () => Promise<void>;
|
||||||
|
fetchCampaigns: (userId: string, workspaceId: string) => Promise<void>;
|
||||||
|
createCampaign: (userId: string, workspaceId: string, name: string) => Promise<string | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useBacklinkOutreachStore = create<BacklinkOutreachStore>((set) => ({
|
export const useBacklinkOutreachStore = create<BacklinkOutreachStore>((set) => ({
|
||||||
modules: [],
|
modules: [],
|
||||||
coverage: null,
|
coverage: null,
|
||||||
|
campaigns: [],
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
refreshBacklinkRegistry: async () => {
|
refreshBacklinkRegistry: async () => {
|
||||||
@@ -35,4 +42,33 @@ export const useBacklinkOutreachStore = create<BacklinkOutreachStore>((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;
|
||||||
|
}
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user