Add missing columns and fix formatting
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -8,24 +7,6 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { DateTime } from "luxon";
|
||||
import { Loader2, AlertCircle } from "lucide-react";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { TIME_RANGES } from "@/lib/constants";
|
||||
|
||||
// Helper functions for formatting
|
||||
const formatRate = (value) => {
|
||||
@@ -55,6 +36,13 @@ const TableSkeleton = () => (
|
||||
</div>
|
||||
);
|
||||
|
||||
// Error alert component
|
||||
const ErrorAlert = ({ description }) => (
|
||||
<div className="p-4 mb-4 text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/10 rounded-lg">
|
||||
{description}
|
||||
</div>
|
||||
);
|
||||
|
||||
// MetricCell component for displaying campaign metrics
|
||||
const MetricCell = ({
|
||||
value,
|
||||
@@ -76,45 +64,58 @@ const MetricCell = ({
|
||||
</td>
|
||||
);
|
||||
|
||||
const KlaviyoCampaigns = ({
|
||||
className,
|
||||
timeRange = "last7days",
|
||||
onTimeRangeChange,
|
||||
title = "Email Campaigns",
|
||||
description
|
||||
}) => {
|
||||
const KlaviyoCampaigns = ({ className, timeRange = "last7days" }) => {
|
||||
const [campaigns, setCampaigns] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [selectedTimeRange, setSelectedTimeRange] = useState(timeRange);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCampaigns();
|
||||
}, [selectedTimeRange]);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [sortConfig, setSortConfig] = useState({
|
||||
key: "send_time",
|
||||
direction: "desc",
|
||||
});
|
||||
|
||||
const fetchCampaigns = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setIsLoading(true);
|
||||
const response = await fetch(`/api/klaviyo/reporting/campaigns/${timeRange}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch campaigns: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setCampaigns(data.data || []);
|
||||
setError(null);
|
||||
|
||||
const response = await axios.get(`/api/klaviyo/reporting/campaigns/${selectedTimeRange}`);
|
||||
setCampaigns(response.data.data || []);
|
||||
} catch (error) {
|
||||
console.error("Error fetching campaigns:", error);
|
||||
setError(error.message);
|
||||
} catch (err) {
|
||||
console.error("Error fetching campaigns:", err);
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTimeRangeChange = (value) => {
|
||||
setSelectedTimeRange(value);
|
||||
if (onTimeRangeChange) {
|
||||
onTimeRangeChange(value);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
fetchCampaigns();
|
||||
const interval = setInterval(fetchCampaigns, 10 * 60 * 1000); // Refresh every 10 minutes
|
||||
return () => clearInterval(interval);
|
||||
}, [timeRange]);
|
||||
|
||||
if (loading) {
|
||||
// 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));
|
||||
}
|
||||
return direction * (a[sortConfig.key] - b[sortConfig.key]);
|
||||
});
|
||||
|
||||
// Filter campaigns by search term
|
||||
const filteredCampaigns = sortedCampaigns.filter(
|
||||
(campaign) =>
|
||||
campaign?.name?.toLowerCase().includes((searchTerm || "").toLowerCase())
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card className="h-full bg-white dark:bg-gray-900">
|
||||
<CardHeader>
|
||||
@@ -129,85 +130,73 @@ const KlaviyoCampaigns = ({
|
||||
|
||||
return (
|
||||
<Card className="h-full bg-white dark:bg-gray-900">
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>Failed to load campaigns: {error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
{error && <ErrorAlert description={error} />}
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex justify-between items-start">
|
||||
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
||||
{title}
|
||||
Email Campaigns
|
||||
</CardTitle>
|
||||
<Select value={selectedTimeRange} onValueChange={handleTimeRangeChange}>
|
||||
<SelectTrigger className="w-[130px]">
|
||||
<SelectValue placeholder="Select time range" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{TIME_RANGES.map((range) => (
|
||||
<SelectItem key={range.value} value={range.value}>
|
||||
{range.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="overflow-y-auto pl-4 max-h-[350px]">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="font-medium">Campaign</TableHead>
|
||||
<TableHead className="text-center font-medium">Delivery</TableHead>
|
||||
<TableHead className="text-center font-medium">Opens</TableHead>
|
||||
<TableHead className="text-center font-medium">Clicks</TableHead>
|
||||
<TableHead className="text-center font-medium">Orders</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{campaigns.map((campaign) => (
|
||||
<TableRow key={campaign.id}>
|
||||
<TableCell className="align-top">
|
||||
<CardContent className="overflow-y-auto pl-4 max-h-[350px] mb-4">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="p-2 text-left font-medium sticky top-0 bg-white dark:bg-gray-900 z-10 text-gray-900 dark:text-gray-100">
|
||||
Campaign
|
||||
</th>
|
||||
<th className="p-2 font-medium text-center sticky top-0 bg-white dark:bg-gray-900 z-10 text-gray-900 dark:text-gray-100">
|
||||
Delivery
|
||||
</th>
|
||||
<th className="p-2 font-medium text-center sticky top-0 bg-white dark:bg-gray-900 z-10 text-gray-900 dark:text-gray-100">
|
||||
Opens
|
||||
</th>
|
||||
<th className="p-2 font-medium text-center sticky top-0 bg-white dark:bg-gray-900 z-10 text-gray-900 dark:text-gray-100">
|
||||
Clicks
|
||||
</th>
|
||||
<th className="p-2 font-medium text-center sticky top-0 bg-white dark:bg-gray-900 z-10 text-gray-900 dark:text-gray-100">
|
||||
CTR
|
||||
</th>
|
||||
<th className="p-2 font-medium text-center sticky top-0 bg-white dark:bg-gray-900 z-10 text-gray-900 dark:text-gray-100">
|
||||
Orders
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-gray-800">
|
||||
{filteredCampaigns.map((campaign) => (
|
||||
<tr
|
||||
key={campaign.id}
|
||||
className="hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<td className="p-2 align-top">
|
||||
<div className="font-medium text-gray-900 dark:text-gray-100">
|
||||
{campaign.name || "Unnamed Campaign"}
|
||||
{campaign.name}
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400 truncate max-w-[300px]">
|
||||
{campaign.subject || "No subject"}
|
||||
{campaign.subject}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 dark:text-gray-500">
|
||||
{campaign.send_time
|
||||
? DateTime.fromISO(campaign.send_time).toLocaleString(
|
||||
DateTime.DATETIME_MED
|
||||
)
|
||||
? DateTime.fromISO(campaign.send_time).toLocaleString(DateTime.DATETIME_MED)
|
||||
: "No date"}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="top"
|
||||
className="break-words bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 border dark:border-gray-700"
|
||||
>
|
||||
<p className="font-medium">
|
||||
{campaign.name || "Unnamed Campaign"}
|
||||
</p>
|
||||
<p>{campaign.subject || "No subject"}</p>
|
||||
<p className="font-medium">{campaign.name}</p>
|
||||
<p>{campaign.subject}</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{campaign.send_time
|
||||
? DateTime.fromISO(campaign.send_time).toLocaleString(
|
||||
DateTime.DATETIME_MED
|
||||
)
|
||||
? DateTime.fromISO(campaign.send_time).toLocaleString(DateTime.DATETIME_MED)
|
||||
: "No date"}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</TableCell>
|
||||
<MetricCell
|
||||
value={campaign.stats.delivery_rate}
|
||||
count={campaign.stats.delivered}
|
||||
@@ -223,6 +212,11 @@ const KlaviyoCampaigns = ({
|
||||
count={campaign.stats.clicks_unique}
|
||||
totalRecipients={campaign.stats.recipients}
|
||||
/>
|
||||
<MetricCell
|
||||
value={campaign.stats.click_to_open_rate}
|
||||
count={campaign.stats.clicks_unique}
|
||||
totalRecipients={campaign.stats.opens_unique}
|
||||
/>
|
||||
<MetricCell
|
||||
value={campaign.stats.conversion_value}
|
||||
count={campaign.stats.conversion_uniques}
|
||||
@@ -230,10 +224,10 @@ const KlaviyoCampaigns = ({
|
||||
showConversionRate={true}
|
||||
totalRecipients={campaign.stats.recipients}
|
||||
/>
|
||||
</TableRow>
|
||||
</tr>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</tbody>
|
||||
</table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user