Separate out blog posts for filtering + standardize campaign table styling
This commit is contained in:
@@ -16,7 +16,7 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { TIME_RANGES } from "@/lib/constants";
|
||||
import { Mail, MessageSquare, ArrowUpDown } from "lucide-react";
|
||||
import { Mail, MessageSquare, ArrowUpDown, BookOpen } from "lucide-react";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
// Helper functions for formatting
|
||||
@@ -41,67 +41,67 @@ const TableSkeleton = () => (
|
||||
<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">
|
||||
<Skeleton className="h-8 w-24 dark:bg-gray-700" />
|
||||
<th className="p-2 text-left font-medium sticky top-0 bg-white dark:bg-gray-900/60 backdrop-blur-sm z-10">
|
||||
<Skeleton className="h-8 w-24 bg-muted" />
|
||||
</th>
|
||||
<th className="p-2 text-center font-medium sticky top-0 bg-white dark:bg-gray-900 z-10">
|
||||
<Skeleton className="h-8 w-20 mx-auto dark:bg-gray-700" />
|
||||
<th className="p-2 text-center font-medium sticky top-0 bg-white dark:bg-gray-900/60 backdrop-blur-sm z-10">
|
||||
<Skeleton className="h-8 w-20 mx-auto bg-muted" />
|
||||
</th>
|
||||
<th className="p-2 text-center font-medium sticky top-0 bg-white dark:bg-gray-900 z-10">
|
||||
<Skeleton className="h-8 w-20 mx-auto dark:bg-gray-700" />
|
||||
<th className="p-2 text-center font-medium sticky top-0 bg-white dark:bg-gray-900/60 backdrop-blur-sm z-10">
|
||||
<Skeleton className="h-8 w-20 mx-auto bg-muted" />
|
||||
</th>
|
||||
<th className="p-2 text-center font-medium sticky top-0 bg-white dark:bg-gray-900 z-10">
|
||||
<Skeleton className="h-8 w-20 mx-auto dark:bg-gray-700" />
|
||||
<th className="p-2 text-center font-medium sticky top-0 bg-white dark:bg-gray-900/60 backdrop-blur-sm z-10">
|
||||
<Skeleton className="h-8 w-20 mx-auto bg-muted" />
|
||||
</th>
|
||||
<th className="p-2 text-center font-medium sticky top-0 bg-white dark:bg-gray-900 z-10">
|
||||
<Skeleton className="h-8 w-20 mx-auto dark:bg-gray-700" />
|
||||
<th className="p-2 text-center font-medium sticky top-0 bg-white dark:bg-gray-900/60 backdrop-blur-sm z-10">
|
||||
<Skeleton className="h-8 w-20 mx-auto bg-muted" />
|
||||
</th>
|
||||
<th className="p-2 text-center font-medium sticky top-0 bg-white dark:bg-gray-900 z-10">
|
||||
<Skeleton className="h-8 w-20 mx-auto dark:bg-gray-700" />
|
||||
<th className="p-2 text-center font-medium sticky top-0 bg-white dark:bg-gray-900/60 backdrop-blur-sm z-10">
|
||||
<Skeleton className="h-8 w-20 mx-auto bg-muted" />
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-gray-800">
|
||||
{[...Array(15)].map((_, i) => (
|
||||
<tr key={i} className="hover:bg-gray-50 dark:hover:bg-gray-800">
|
||||
<tr key={i} className="hover:bg-muted/50 transition-colors">
|
||||
<td className="p-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="h-4 w-4 dark:bg-gray-700" />
|
||||
<Skeleton className="h-4 w-4 bg-muted" />
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-48 dark:bg-gray-700" />
|
||||
<Skeleton className="h-3 w-64 dark:bg-gray-700" />
|
||||
<Skeleton className="h-3 w-32 dark:bg-gray-700" />
|
||||
<Skeleton className="h-4 w-48 bg-muted" />
|
||||
<Skeleton className="h-3 w-64 bg-muted" />
|
||||
<Skeleton className="h-3 w-32 bg-muted" />
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-2 text-center">
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<Skeleton className="h-4 w-16 dark:bg-gray-700" />
|
||||
<Skeleton className="h-3 w-24 dark:bg-gray-700" />
|
||||
<Skeleton className="h-4 w-16 bg-muted" />
|
||||
<Skeleton className="h-3 w-24 bg-muted" />
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-2 text-center">
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<Skeleton className="h-4 w-16 dark:bg-gray-700" />
|
||||
<Skeleton className="h-3 w-24 dark:bg-gray-700" />
|
||||
<Skeleton className="h-4 w-16 bg-muted" />
|
||||
<Skeleton className="h-3 w-24 bg-muted" />
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-2 text-center">
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<Skeleton className="h-4 w-16 dark:bg-gray-700" />
|
||||
<Skeleton className="h-3 w-24 dark:bg-gray-700" />
|
||||
<Skeleton className="h-4 w-16 bg-muted" />
|
||||
<Skeleton className="h-3 w-24 bg-muted" />
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-2 text-center">
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<Skeleton className="h-4 w-16 dark:bg-gray-700" />
|
||||
<Skeleton className="h-3 w-24 dark:bg-gray-700" />
|
||||
<Skeleton className="h-4 w-16 bg-muted" />
|
||||
<Skeleton className="h-3 w-24 bg-muted" />
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-2 text-center">
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<Skeleton className="h-4 w-16 dark:bg-gray-700" />
|
||||
<Skeleton className="h-3 w-24 dark:bg-gray-700" />
|
||||
<Skeleton className="h-4 w-16 bg-muted" />
|
||||
<Skeleton className="h-3 w-24 bg-muted" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -112,7 +112,7 @@ const TableSkeleton = () => (
|
||||
|
||||
// Error alert component
|
||||
const ErrorAlert = ({ description }) => (
|
||||
<div className="p-4 m-6 text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/10 rounded-lg">
|
||||
<div className="p-4 m-6 text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/10 rounded-lg border border-red-200 dark:border-red-900/20">
|
||||
{description}
|
||||
</div>
|
||||
);
|
||||
@@ -130,8 +130,8 @@ const MetricCell = ({
|
||||
if (isSMS && hideForSMS) {
|
||||
return (
|
||||
<td className="p-2 text-center">
|
||||
<div className="text-gray-500 dark:text-gray-400 text-lg font-semibold">N/A</div>
|
||||
<div className="text-gray-400 dark:text-gray-500 text-sm">-</div>
|
||||
<div className="text-muted-foreground text-lg font-semibold">N/A</div>
|
||||
<div className="text-muted-foreground text-sm">-</div>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
@@ -141,7 +141,7 @@ const MetricCell = ({
|
||||
<div className="text-blue-600 dark:text-blue-400 text-lg font-semibold">
|
||||
{isMonetary ? formatCurrency(value) : formatRate(value, isSMS, hideForSMS)}
|
||||
</div>
|
||||
<div className="text-gray-600 dark:text-gray-400 text-sm">
|
||||
<div className="text-muted-foreground text-sm">
|
||||
{count?.toLocaleString() || 0} {count === 1 ? "recipient" : "recipients"}
|
||||
{showConversionRate &&
|
||||
totalRecipients > 0 &&
|
||||
@@ -156,7 +156,7 @@ const KlaviyoCampaigns = ({ className }) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [selectedChannels, setSelectedChannels] = useState({ email: true, sms: true });
|
||||
const [selectedChannels, setSelectedChannels] = useState({ email: true, sms: true, blog: true });
|
||||
const [selectedTimeRange, setSelectedTimeRange] = useState("last7days");
|
||||
const [sortConfig, setSortConfig] = useState({
|
||||
key: "send_time",
|
||||
@@ -222,25 +222,28 @@ const KlaviyoCampaigns = ({ className }) => {
|
||||
|
||||
// Filter campaigns by search term and channels
|
||||
const filteredCampaigns = sortedCampaigns.filter(
|
||||
(campaign) =>
|
||||
campaign?.name?.toLowerCase().includes((searchTerm || "").toLowerCase()) &&
|
||||
selectedChannels[campaign?.channel]
|
||||
(campaign) => {
|
||||
const isBlog = campaign?.name?.includes("_Blog");
|
||||
const channelType = isBlog ? "blog" : campaign?.channel;
|
||||
return campaign?.name?.toLowerCase().includes((searchTerm || "").toLowerCase()) &&
|
||||
selectedChannels[channelType];
|
||||
}
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card className="h-full bg-white dark:bg-gray-900">
|
||||
<Card className="h-full bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
||||
<Skeleton className="h-6 w-48 dark:bg-gray-700" />
|
||||
<Skeleton className="h-6 w-48 bg-muted" />
|
||||
</CardTitle>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex ml-1 gap-1 items-center">
|
||||
<Skeleton className="h-8 w-20 dark:bg-gray-700" />
|
||||
<Skeleton className="h-8 w-20 dark:bg-gray-700" />
|
||||
<Skeleton className="h-8 w-20 bg-muted" />
|
||||
<Skeleton className="h-8 w-20 bg-muted" />
|
||||
</div>
|
||||
<Skeleton className="h-8 w-[130px] dark:bg-gray-700" />
|
||||
<Skeleton className="h-8 w-[130px] bg-muted" />
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -252,7 +255,7 @@ const KlaviyoCampaigns = ({ className }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="h-full bg-white dark:bg-gray-900">
|
||||
<Card className="h-full bg-white dark:bg-gray-900/60 backdrop-blur-sm">
|
||||
{error && <ErrorAlert description={error} />}
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex justify-between items-center">
|
||||
@@ -277,6 +280,14 @@ const KlaviyoCampaigns = ({ className }) => {
|
||||
<MessageSquare className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">SMS</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant={selectedChannels.blog ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => setSelectedChannels(prev => ({ ...prev, blog: !prev.blog }))}
|
||||
>
|
||||
<BookOpen className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Blog</span>
|
||||
</Button>
|
||||
</div>
|
||||
<Select value={selectedTimeRange} onValueChange={setSelectedTimeRange}>
|
||||
<SelectTrigger className="w-[130px]">
|
||||
@@ -297,7 +308,7 @@ const KlaviyoCampaigns = ({ className }) => {
|
||||
<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">
|
||||
<th className="p-2 text-left font-medium sticky top-0 bg-white dark:bg-gray-900/60 backdrop-blur-sm z-10 text-gray-900 dark:text-gray-100">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => handleSort("send_time")}
|
||||
@@ -306,7 +317,7 @@ const KlaviyoCampaigns = ({ className }) => {
|
||||
Campaign
|
||||
</Button>
|
||||
</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">
|
||||
<th className="p-2 font-medium text-center sticky top-0 bg-white dark:bg-gray-900/60 backdrop-blur-sm z-10 text-gray-900 dark:text-gray-100">
|
||||
<Button
|
||||
variant={sortConfig.key === "delivery_rate" ? "default" : "ghost"}
|
||||
onClick={() => handleSort("delivery_rate")}
|
||||
@@ -315,7 +326,7 @@ const KlaviyoCampaigns = ({ className }) => {
|
||||
Delivery
|
||||
</Button>
|
||||
</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">
|
||||
<th className="p-2 font-medium text-center sticky top-0 bg-white dark:bg-gray-900/60 backdrop-blur-sm z-10 text-gray-900 dark:text-gray-100">
|
||||
<Button
|
||||
variant={sortConfig.key === "open_rate" ? "default" : "ghost"}
|
||||
onClick={() => handleSort("open_rate")}
|
||||
@@ -324,7 +335,7 @@ const KlaviyoCampaigns = ({ className }) => {
|
||||
Opens
|
||||
</Button>
|
||||
</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">
|
||||
<th className="p-2 font-medium text-center sticky top-0 bg-white dark:bg-gray-900/60 backdrop-blur-sm z-10 text-gray-900 dark:text-gray-100">
|
||||
<Button
|
||||
variant={sortConfig.key === "click_rate" ? "default" : "ghost"}
|
||||
onClick={() => handleSort("click_rate")}
|
||||
@@ -333,7 +344,7 @@ const KlaviyoCampaigns = ({ className }) => {
|
||||
Clicks
|
||||
</Button>
|
||||
</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">
|
||||
<th className="p-2 font-medium text-center sticky top-0 bg-white dark:bg-gray-900/60 backdrop-blur-sm z-10 text-gray-900 dark:text-gray-100">
|
||||
<Button
|
||||
variant={sortConfig.key === "click_to_open_rate" ? "default" : "ghost"}
|
||||
onClick={() => handleSort("click_to_open_rate")}
|
||||
@@ -342,7 +353,7 @@ const KlaviyoCampaigns = ({ className }) => {
|
||||
CTR
|
||||
</Button>
|
||||
</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">
|
||||
<th className="p-2 font-medium text-center sticky top-0 bg-white dark:bg-gray-900/60 backdrop-blur-sm z-10 text-gray-900 dark:text-gray-100">
|
||||
<Button
|
||||
variant={sortConfig.key === "conversion_value" ? "default" : "ghost"}
|
||||
onClick={() => handleSort("conversion_value")}
|
||||
@@ -357,26 +368,28 @@ const KlaviyoCampaigns = ({ className }) => {
|
||||
{filteredCampaigns.map((campaign) => (
|
||||
<tr
|
||||
key={campaign.id}
|
||||
className="hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||||
className="hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<td className="p-2 align-top">
|
||||
<div className="flex items-center gap-2">
|
||||
{campaign.channel === 'sms' ? (
|
||||
<MessageSquare className="h-4 w-4 text-gray-500" />
|
||||
{campaign.name?.includes("_Blog") ? (
|
||||
<BookOpen className="h-4 w-4 text-muted-foreground" />
|
||||
) : campaign.channel === 'sms' ? (
|
||||
<MessageSquare className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<Mail className="h-4 w-4 text-gray-500" />
|
||||
<Mail className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
<div className="font-medium text-gray-900 dark:text-gray-100">
|
||||
{campaign.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400 truncate max-w-[300px]">
|
||||
<div className="text-sm text-muted-foreground truncate max-w-[300px]">
|
||||
{campaign.subject}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 dark:text-gray-500">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{campaign.send_time
|
||||
? DateTime.fromISO(campaign.send_time).toLocaleString(DateTime.DATETIME_MED)
|
||||
: "No date"}
|
||||
@@ -385,11 +398,11 @@ const KlaviyoCampaigns = ({ className }) => {
|
||||
</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"
|
||||
className="break-words bg-white dark:bg-gray-900/60 backdrop-blur-sm text-gray-900 dark:text-gray-100 border dark:border-gray-800"
|
||||
>
|
||||
<p className="font-medium">{campaign.name}</p>
|
||||
<p>{campaign.subject}</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{campaign.send_time
|
||||
? DateTime.fromISO(campaign.send_time).toLocaleString(DateTime.DATETIME_MED)
|
||||
: "No date"}
|
||||
|
||||
Reference in New Issue
Block a user