import React, { useEffect, useState, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { listCampaigns, deleteCampaign, activateCampaign, pauseCampaign, resumeCampaign, completeCampaign, } from '../service/campaigns/campaigns.service'; import type { ICampaign } from '../service/campaigns/campaigns.interface'; const STATUS_COLORS: Record = { active: 'bg-green-100 text-green-800', paused: 'bg-yellow-100 text-yellow-800', building: 'bg-blue-100 text-blue-800', draft: 'bg-gray-100 text-gray-800', completed: 'bg-gray-100 text-gray-800', failed: 'bg-red-100 text-red-800', }; const Campaigns = (): JSX.Element => { const navigate = useNavigate(); const [campaigns, setCampaigns] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [actionLoading, setActionLoading] = useState(null); const fetchCampaigns = useCallback(async () => { try { setLoading(true); setError(null); const response = await listCampaigns(); if (!response.errors && response.data) { setCampaigns(response.data.campaigns || []); } } catch (err: any) { setError(err?.message || 'Failed to load campaigns'); } finally { setLoading(false); } }, []); useEffect(() => { fetchCampaigns(); }, [fetchCampaigns]); // The Reco copilot dispatches ``copilot:data-changed`` after a turn where a // write tool ran (create_campaign, activate/pause/complete, etc.). WordPress // has no route loaders to revalidate, so this page re-fetches its list when // the event fires - mirroring the Remix ``revalidator.revalidate()`` the // copilot used in the Shopify plugin. The building-status poll below then // takes over for any campaign still indexing. The listener is removed on // unmount so it never fires against a stale component. useEffect(() => { const onCopilotDataChanged = (): void => { void fetchCampaigns(); }; window.addEventListener('copilot:data-changed', onCopilotDataChanged); return () => { window.removeEventListener('copilot:data-changed', onCopilotDataChanged); }; }, [fetchCampaigns]); // Poll while any campaign is building useEffect(() => { const hasBuilding = campaigns.some(c => c.status === 'building'); if (!hasBuilding) return; const interval = setInterval(() => { fetchCampaigns(); }, 5000); return () => clearInterval(interval); }, [campaigns, fetchCampaigns]); const handleAction = async ( campaignId: string, action: 'activate' | 'pause' | 'resume' | 'complete' | 'delete', e: React.MouseEvent ) => { e.stopPropagation(); const actions = { activate: activateCampaign, pause: pauseCampaign, resume: resumeCampaign, complete: completeCampaign, delete: deleteCampaign, }; try { setActionLoading(campaignId); await actions[action](campaignId); await fetchCampaigns(); } catch (err: any) { setError(err?.message || `Failed to ${action} campaign`); } finally { setActionLoading(null); } }; const activeCampaign = campaigns.find(c => c.status === 'active'); return (

Campaigns

{error && (

{error}

)} {activeCampaign && (

Active campaign: {activeCampaign.name} -{' '} {activeCampaign.indexed_product_count} products indexed

)} {loading ? (
) : campaigns.length === 0 ? (

No campaigns yet. Create your first campaign to boost specific product categories in chat conversations.

) : (
{campaigns.map(campaign => (
navigate(`/campaigns/${campaign.campaign_id}`)} >

{campaign.name}

{campaign.status} {campaign.priority}
{campaign.status === 'draft' && ( )} {campaign.status === 'active' && ( )} {campaign.status === 'paused' && ( )} {['active', 'paused'].includes(campaign.status) && ( )} {['draft', 'failed', 'completed'].includes( campaign.status ) && ( )}
Categories: {campaign.target_categories.join(', ')} {campaign.indexed_product_count > 0 && ( {campaign.indexed_product_count} products indexed )}
{campaign.report && (
{campaign.report.impressions} impressions {campaign.report.clicks} clicks {campaign.report.add_to_cart} add to cart CTR: {campaign.report.ctr}%
)}
))}
)}
); }; export default Campaigns;