import React, { useState, useEffect, useRef } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { DateTime } from "luxon"; // Helper functions for formatting const formatRate = (value) => { if (typeof value !== "number") return "0.0%"; return `${(value * 100).toFixed(1)}%`; }; const formatCurrency = (value) => { if (typeof value !== "number") return "$0"; return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 0, maximumFractionDigits: 0, }).format(value); }; // Loading skeleton component const TableSkeleton = () => (
{[...Array(5)].map((_, i) => (
))}
); // Error alert component const ErrorAlert = ({ description }) => (
{description}
); // MetricCell component for displaying campaign metrics const MetricCell = ({ value, count, isMonetary = false, showConversionRate = false, totalRecipients = 0, }) => (
{isMonetary ? formatCurrency(value) : formatRate(value)}
{count?.toLocaleString() || 0} {count === 1 ? "recipient" : "recipients"} {showConversionRate && totalRecipients > 0 && ` (${((count / totalRecipients) * 100).toFixed(2)}%)`}
); const KlaviyoCampaigns = ({ className, timeRange = "last7days" }) => { const [campaigns, setCampaigns] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [searchTerm, setSearchTerm] = useState(""); const [sortConfig, setSortConfig] = useState({ key: "send_time", direction: "desc", }); const fetchInProgress = useRef(false); const fetchCampaigns = async () => { console.log("Component fetching campaigns...", { timeRange, timestamp: new Date().toISOString() }); try { setIsLoading(true); const response = await fetch(`/api/klaviyo/campaigns/${timeRange}`); if (!response.ok) { const errorText = await response.text(); console.error("Campaign fetch error response:", errorText); throw new Error(`Failed to fetch campaigns: ${response.status}`); } const data = await response.json(); console.log("Received campaign data:", { type: data?.type, count: data?.data?.length, sample: data?.data?.[0], structure: { topLevel: Object.keys(data || {}), firstCampaign: data?.data?.[0] ? Object.keys(data.data[0]) : null, stats: data?.data?.[0]?.stats ? Object.keys(data.data[0].stats) : null } }); // Handle the new data structure const campaignsData = data?.data || []; if (!Array.isArray(campaignsData)) { throw new Error('Invalid campaign data format received'); } // Process campaigns to ensure consistent structure const processedCampaigns = campaignsData.map(campaign => ({ id: campaign.id, name: campaign.name || "Unnamed Campaign", subject: campaign.subject || "", send_time: campaign.send_time, stats: { delivery_rate: campaign.stats?.delivery_rate || 0, delivered: campaign.stats?.delivered || 0, recipients: campaign.stats?.recipients || 0, open_rate: campaign.stats?.open_rate || 0, opens_unique: campaign.stats?.opens_unique || 0, opens: campaign.stats?.opens || 0, clicks_unique: campaign.stats?.clicks_unique || 0, click_rate: campaign.stats?.click_rate || 0, click_to_open_rate: campaign.stats?.click_to_open_rate || 0, conversion_value: campaign.stats?.conversion_value || 0, conversion_uniques: campaign.stats?.conversion_uniques || 0 } })); console.log("Processed campaigns:", { count: processedCampaigns.length, sample: processedCampaigns[0] }); setCampaigns(processedCampaigns); setError(null); } catch (err) { console.error("Error fetching campaigns:", err); setError(err.message); } finally { setIsLoading(false); } }; useEffect(() => { fetchCampaigns(); const interval = setInterval(fetchCampaigns, 10 * 60 * 1000); // Refresh every 10 minutes return () => clearInterval(interval); }, [timeRange]); // Add this to debug render console.log("Rendering campaigns:", { count: campaigns?.length, isLoading, error }); // Sort campaigns const sortedCampaigns = [...campaigns].sort((a, b) => { const direction = sortConfig.direction === "desc" ? -1 : 1; if (sortConfig.key === "send_time") { return ( direction * (DateTime.fromISO(a.send_time) - DateTime.fromISO(b.send_time)) ); } // Handle nested stats properties if (sortConfig.key.startsWith("stats.")) { const statKey = sortConfig.key.split(".")[1]; return direction * (a.stats[statKey] - b.stats[statKey]); } return direction * (a[sortConfig.key] - b[sortConfig.key]); }); // Filter campaigns by search term const filteredCampaigns = sortedCampaigns.filter( (campaign) => campaign && campaign.name && // verify campaign and name exist campaign.name.toLowerCase().includes((searchTerm || "").toLowerCase()) ); if (isLoading) { return (
); } return ( {error && } Email Campaigns {filteredCampaigns.map( (campaign) => campaign && (

{campaign.name || "Unnamed Campaign"}

{campaign.subject || "No subject"}

{campaign.send_time ? DateTime.fromISO( campaign.send_time ).toLocaleString(DateTime.DATETIME_MED) : "No date"}

) )}
Campaign Delivery Opens Clicks CTR Orders
{campaign.name || "Unnamed Campaign"}
{campaign.subject || "No subject"}
{campaign.send_time ? DateTime.fromISO( campaign.send_time ).toLocaleString(DateTime.DATETIME_MED) : "No date"}
); }; export default KlaviyoCampaigns;