Reuse campaign-creator persistence pattern for backlink campaigns

This commit is contained in:
ي
2026-05-11 15:09:17 +05:30
parent 3f984e8d0c
commit 020b237e57
13 changed files with 699 additions and 2 deletions

View File

@@ -0,0 +1,80 @@
import { apiClient } from './client';
export interface BacklinkModuleRecord {
identifier: 'backlink' | 'outreach' | 'guest_post' | string;
module_path: string;
purpose: string;
}
export interface BacklinkModuleRegistryResponse {
feature: string;
modules: BacklinkModuleRecord[];
}
export interface BacklinkCoverageResponse {
legacy_reference: string;
implemented_count: number;
planned_count: number;
implemented: string[];
planned: string[];
}
export interface BacklinkQueryTemplatesResponse {
keyword: string;
queries: string[];
}
export interface BacklinkDiscoveryRequest {
keyword: string;
max_results?: number;
}
export interface BacklinkOpportunity {
url: string;
title: string;
snippet: string;
confidence_score: number;
}
export interface BacklinkPolicyValidationRequest {
user_id: string;
workspace_id: string;
campaign_id: string;
recipient_email: string;
recipient_domain: string;
recipient_region: string;
legal_basis: string;
approved_by_human: boolean;
unsubscribe_url?: string;
sender_identity: string;
idempotency_key: string;
}
export interface BacklinkPolicyValidationResponse {
allowed: boolean;
reasons: string[];
final_status: string;
}
export interface BacklinkReportingSnapshot {
send_volume: number;
decision_events: number;
response_rate: number;
placement_conversion: number;
}
export interface BacklinkDiscoveryResponse {
keyword: string;
queries: string[];
opportunities: BacklinkOpportunity[];
}
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 fetchBacklinkQueryTemplates = async (keyword: string): Promise<BacklinkQueryTemplatesResponse> => (await apiClient.get('/api/backlink-outreach/query-templates', { params: { keyword } })).data;
export const discoverBacklinkOpportunities = async (payload: BacklinkDiscoveryRequest): Promise<BacklinkDiscoveryResponse> => (await apiClient.post('/api/backlink-outreach/discover', 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;

View File

@@ -0,0 +1,60 @@
import React, { 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 [queryPreview, setQueryPreview] = useState<string[]>([]);
useEffect(() => {
refreshBacklinkRegistry();
}, [refreshBacklinkRegistry]);
useEffect(() => {
const loadPreview = async () => {
const response = await fetchBacklinkQueryTemplates('seo');
setQueryPreview(response.queries.slice(0, 3));
};
loadPreview().catch(() => setQueryPreview([]));
}, []);
return (
<section>
<h3>Backlink Outreach Modules</h3>
{isLoading && <p>Loading backlink module registry</p>}
{error && <p>{error}</p>}
{!isLoading && !error && (
<>
<ul>
{modules.map((module) => (
<li key={`${module.identifier}:${module.module_path}`}>
<strong>{module.identifier}</strong>: {module.module_path}
</li>
))}
</ul>
{coverage && (
<>
<p>
Legacy parity: {coverage.implemented_count} implemented / {coverage.planned_count} planned
</p>
<p>Legacy reference: {coverage.legacy_reference}</p>
</>
)}
{queryPreview.length > 0 && (
<>
<h4>Legacy query template preview (keyword: seo)</h4>
<ul>
{queryPreview.map((query) => (
<li key={query}>{query}</li>
))}
</ul>
</>
)}
</>
)}
</section>
);
};
export default BacklinkOutreachModuleList;

View File

@@ -0,0 +1,38 @@
import { create } from 'zustand';
import {
BacklinkCoverageResponse,
BacklinkModuleRecord,
fetchBacklinkMigrationCoverage,
fetchBacklinkModuleRegistry,
} from '../api/backlinkOutreachApi';
interface BacklinkOutreachStore {
modules: BacklinkModuleRecord[];
coverage: BacklinkCoverageResponse | null;
isLoading: boolean;
error: string | null;
refreshBacklinkRegistry: () => Promise<void>;
}
export const useBacklinkOutreachStore = create<BacklinkOutreachStore>((set) => ({
modules: [],
coverage: null,
isLoading: false,
error: null,
refreshBacklinkRegistry: async () => {
set({ isLoading: true, error: null });
try {
const [registryPayload, coveragePayload] = await Promise.all([
fetchBacklinkModuleRegistry(),
fetchBacklinkMigrationCoverage(),
]);
set({ modules: registryPayload.modules, coverage: coveragePayload, isLoading: false });
} catch (error: any) {
set({
isLoading: false,
error: error?.message ?? 'Failed to load backlink module registry',
});
}
},
}));

View File

@@ -8,4 +8,5 @@ export { useSemanticDashboardStore } from './semanticDashboardStore';
export type { DashboardStore } from './dashboardStore';
export type { SEODashboardStore } from './seoDashboardStore';
export type { SharedDashboardState } from './sharedDashboardStore';
export type { SemanticDashboardStore } from './semanticDashboardStore';
export type { SemanticDashboardStore } from './semanticDashboardStore';
export { useBacklinkOutreachStore } from './backlinkOutreachStore';