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