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