Add campaigns component and services
This commit is contained in:
@@ -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">
|
||||
|
||||
242
dashboard/src/components/dashboard/KlaviyoCampaigns.jsx
Normal file
242
dashboard/src/components/dashboard/KlaviyoCampaigns.jsx
Normal 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;
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user