Add campaigns component and services

This commit is contained in:
2024-12-27 01:32:48 -05:00
parent aaa95a5b9e
commit e8b5f8ce07
13 changed files with 883 additions and 24 deletions

View File

@@ -21,6 +21,7 @@ import EventFeed from "./components/dashboard/EventFeed";
import StatCards from "./components/dashboard/StatCards";
import ProductGrid from "./components/dashboard/ProductGrid";
import SalesChart from "./components/dashboard/SalesChart";
import KlaviyoCampaigns from "@/components/dashboard/KlaviyoCampaigns";
// Public layout
const PublicLayout = () => (
@@ -88,6 +89,7 @@ const DashboardLayout = () => {
</div>
<Navigation />
<div className="p-4 space-y-4">
<KlaviyoCampaigns />
<div className="grid grid-cols-1 xl:grid-cols-6 gap-4">
<div className="xl:col-span-4 col-span-6">
<div className="space-y-4 h-full w-full">

View File

@@ -0,0 +1,242 @@
import React, { useState, useEffect } from "react";
import axios from "axios";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Tooltip,
TooltipContent,
TooltipProvider,
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) => {
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 = () => (
<div className="space-y-4">
{[...Array(5)].map((_, i) => (
<div
key={i}
className="h-16 bg-gray-100 dark:bg-gray-800 animate-pulse rounded"
/>
))}
</div>
);
// MetricCell component for displaying campaign metrics
const MetricCell = ({
value,
count,
isMonetary = false,
showConversionRate = false,
totalRecipients = 0,
}) => (
<td className="p-2 text-center">
<div className="text-blue-600 dark:text-blue-400 text-lg font-semibold">
{isMonetary ? formatCurrency(value) : formatRate(value)}
</div>
<div className="text-gray-600 dark:text-gray-400 text-sm">
{count?.toLocaleString() || 0} {count === 1 ? "recipient" : "recipients"}
{showConversionRate &&
totalRecipients > 0 &&
` (${((count / totalRecipients) * 100).toFixed(2)}%)`}
</div>
</td>
);
const KlaviyoCampaigns = ({
className,
timeRange = "last7days",
onTimeRangeChange,
title = "Email Campaigns",
description
}) => {
const [campaigns, setCampaigns] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [selectedTimeRange, setSelectedTimeRange] = useState(timeRange);
useEffect(() => {
fetchCampaigns();
}, [selectedTimeRange]);
const fetchCampaigns = async () => {
try {
setLoading(true);
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);
} finally {
setLoading(false);
}
};
const handleTimeRangeChange = (value) => {
setSelectedTimeRange(value);
if (onTimeRangeChange) {
onTimeRangeChange(value);
}
};
if (loading) {
return (
<Card className="h-full bg-white dark:bg-gray-900">
<CardHeader>
<div className="h-6 w-48 bg-gray-200 dark:bg-gray-700 animate-pulse rounded" />
</CardHeader>
<CardContent className="overflow-y-auto pl-4 max-h-[350px]">
<TableSkeleton />
</CardContent>
</Card>
);
}
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>
)}
<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}
</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">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div>
<div className="font-medium text-gray-900 dark:text-gray-100">
{campaign.name || "Unnamed Campaign"}
</div>
<div className="text-sm text-gray-500 dark:text-gray-400 truncate max-w-[300px]">
{campaign.subject || "No 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
)
: "No date"}
</div>
</div>
</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="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>
</TableCell>
<MetricCell
value={campaign.stats.delivery_rate}
count={campaign.stats.delivered}
totalRecipients={campaign.stats.recipients}
/>
<MetricCell
value={campaign.stats.open_rate}
count={campaign.stats.opens_unique}
totalRecipients={campaign.stats.recipients}
/>
<MetricCell
value={campaign.stats.click_rate}
count={campaign.stats.clicks_unique}
totalRecipients={campaign.stats.recipients}
/>
<MetricCell
value={campaign.stats.conversion_value}
count={campaign.stats.conversion_uniques}
isMonetary={true}
showConversionRate={true}
totalRecipients={campaign.stats.recipients}
/>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
);
};
export default KlaviyoCampaigns;

View File

@@ -2,12 +2,12 @@ export const TIME_RANGES = [
{ value: 'today', label: 'Today' },
{ value: 'yesterday', label: 'Yesterday' },
{ value: 'last7days', label: 'Last 7 Days' },
{ value: 'last14days', label: 'Last 14 Days' },
{ value: 'last30days', label: 'Last 30 Days' },
{ value: 'last90days', label: 'Last 90 Days' },
{ value: 'thisWeek', label: 'This Week' },
{ value: 'lastWeek', label: 'Last Week' },
{ value: 'thisMonth', label: 'This Month' },
{ value: 'lastMonth', label: 'Last Month' }
{ value: 'monthToDate', label: 'Month to Date' },
{ value: 'quarterToDate', label: 'Quarter to Date' },
{ value: 'yearToDate', label: 'Year to Date' },
];
export const GROUP_BY_OPTIONS = [

View File

@@ -117,21 +117,6 @@ export default defineConfig(({ mode }) => {
});
});
},
onProxyReq: (proxyReq, req, res) => {
// Log the outgoing request
console.log("Proxy request:", {
method: req.method,
path: req.path,
headers: req.headers,
});
},
onProxyRes: (proxyRes, req, res) => {
// Log the incoming response
console.log("Proxy response:", {
status: proxyRes.statusCode,
headers: proxyRes.headers,
});
},
},
"/api": {
target: "https://dashboard.kent.pw",