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