First pass at restoring original meta component
This commit is contained in:
448
examples DO NOT USE OR EDIT/EXAMPLE ONLY MetaAdsOverview.jsx
Normal file
448
examples DO NOT USE OR EDIT/EXAMPLE ONLY MetaAdsOverview.jsx
Normal file
@@ -0,0 +1,448 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Instagram, Loader2 } from "lucide-react";
|
||||
import { metaAdsService } from "@/services/metaAdsService";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
const formatCurrency = (value, decimalPlaces = 2) =>
|
||||
new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
minimumFractionDigits: decimalPlaces,
|
||||
maximumFractionDigits: decimalPlaces,
|
||||
}).format(value || 0);
|
||||
|
||||
const formatNumber = (value, decimalPlaces = 0) => {
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
minimumFractionDigits: decimalPlaces,
|
||||
maximumFractionDigits: decimalPlaces,
|
||||
}).format(value || 0);
|
||||
};
|
||||
|
||||
const formatPercent = (value, decimalPlaces = 2) =>
|
||||
`${(value || 0).toFixed(decimalPlaces)}%`;
|
||||
|
||||
const summaryCard = (label, value, options = {}) => {
|
||||
const {
|
||||
isMonetary = false,
|
||||
isPercentage = false,
|
||||
decimalPlaces = 0,
|
||||
} = options;
|
||||
|
||||
let displayValue;
|
||||
if (isMonetary) {
|
||||
displayValue = formatCurrency(value, decimalPlaces);
|
||||
} else if (isPercentage) {
|
||||
displayValue = formatPercent(value, decimalPlaces);
|
||||
} else {
|
||||
displayValue = formatNumber(value, decimalPlaces);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-3 rounded-lg bg-gray-50 dark:bg-gray-800">
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">{label}</div>
|
||||
<div className="text-xl font-bold text-gray-900 dark:text-gray-100">
|
||||
{displayValue}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MetricCell = ({
|
||||
value,
|
||||
label,
|
||||
sublabel,
|
||||
isMonetary = false,
|
||||
isPercentage = false,
|
||||
decimalPlaces = 0,
|
||||
}) => (
|
||||
<td className="p-2 text-center align-top">
|
||||
<div className="text-blue-600 dark:text-blue-400 text-left font-semibold">
|
||||
{isMonetary
|
||||
? formatCurrency(value, decimalPlaces)
|
||||
: isPercentage
|
||||
? formatPercent(value, decimalPlaces)
|
||||
: formatNumber(value, decimalPlaces)}
|
||||
</div>
|
||||
{label && (
|
||||
<div className="text-xs text-left text-gray-500 dark:text-gray-400">
|
||||
{label}
|
||||
</div>
|
||||
)}
|
||||
{sublabel && (
|
||||
<div className="text-xs text-left text-gray-500 dark:text-gray-400">
|
||||
{sublabel}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
);
|
||||
|
||||
const getActionValue = (campaign, actionType) => {
|
||||
if (actionType === "impressions" || actionType === "reach") {
|
||||
return campaign.metrics[actionType] || 0;
|
||||
}
|
||||
|
||||
const actions = campaign.metrics.actions;
|
||||
if (Array.isArray(actions)) {
|
||||
const action = actions.find((a) => a.action_type === actionType);
|
||||
return action ? parseInt(action.value) || 0 : 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
const CampaignName = ({ name }) => {
|
||||
if (name.startsWith("Instagram post: ")) {
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Instagram className="w-4 h-4" />
|
||||
<span>{name.replace("Instagram post: ", "")}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <span>{name}</span>;
|
||||
};
|
||||
|
||||
const MetaAdsOverview = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [campaigns, setCampaigns] = useState([]);
|
||||
const [timeframe, setTimeframe] = useState("30");
|
||||
const [summaryMetrics, setSummaryMetrics] = useState(null);
|
||||
const computeDateRange = (timeframe) => {
|
||||
// Create date in Eastern Time
|
||||
const now = new Date();
|
||||
const easternTime = new Date(
|
||||
now.toLocaleString("en-US", { timeZone: "America/New_York" })
|
||||
);
|
||||
easternTime.setHours(0, 0, 0, 0); // Set to start of day
|
||||
|
||||
let sinceDate, untilDate;
|
||||
|
||||
if (timeframe === "today") {
|
||||
// For today, both dates should be the current date in Eastern Time
|
||||
sinceDate = untilDate = new Date(easternTime);
|
||||
} else {
|
||||
// For other periods, calculate the date range
|
||||
untilDate = new Date(easternTime);
|
||||
untilDate.setDate(untilDate.getDate() - 1); // Yesterday
|
||||
|
||||
sinceDate = new Date(untilDate);
|
||||
sinceDate.setDate(sinceDate.getDate() - parseInt(timeframe) + 1);
|
||||
}
|
||||
|
||||
return {
|
||||
since: sinceDate.toISOString().split("T")[0],
|
||||
until: untilDate.toISOString().split("T")[0],
|
||||
};
|
||||
};
|
||||
useEffect(() => {
|
||||
const fetchMetaAdsData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const { since, until } = computeDateRange(timeframe);
|
||||
|
||||
const [campaignData, accountInsights] = await Promise.all([
|
||||
metaAdsService.getCampaigns({ since, until }),
|
||||
metaAdsService.getAccountInsights({ since, until }),
|
||||
]);
|
||||
|
||||
const activeCampaigns = campaignData.filter((c) => c.metrics.spend > 0);
|
||||
setCampaigns(activeCampaigns);
|
||||
|
||||
const totalReach = parseInt(accountInsights.reach || 0);
|
||||
|
||||
if (activeCampaigns.length > 0) {
|
||||
const totalSpend = activeCampaigns.reduce(
|
||||
(sum, camp) => sum + camp.metrics.spend,
|
||||
0
|
||||
);
|
||||
const totalImpressions = activeCampaigns.reduce(
|
||||
(sum, camp) => sum + camp.metrics.impressions,
|
||||
0
|
||||
);
|
||||
const totalPurchases = activeCampaigns.reduce(
|
||||
(sum, camp) => sum + camp.metrics.totalPurchases,
|
||||
0
|
||||
);
|
||||
const totalPurchaseValue = activeCampaigns.reduce(
|
||||
(sum, camp) => sum + camp.metrics.purchaseValue,
|
||||
0
|
||||
);
|
||||
const totalLinkClicks = activeCampaigns.reduce(
|
||||
(sum, camp) => sum + (camp.metrics.clicks || 0),
|
||||
0
|
||||
);
|
||||
const totalPostEngagements = activeCampaigns.reduce(
|
||||
(sum, camp) => sum + (camp.metrics.totalPostEngagements || 0),
|
||||
0
|
||||
);
|
||||
const totalFrequency = activeCampaigns.reduce(
|
||||
(sum, camp) => sum + camp.metrics.frequency,
|
||||
0
|
||||
);
|
||||
const totalCpm = activeCampaigns.reduce(
|
||||
(sum, camp) => sum + camp.metrics.cpm,
|
||||
0
|
||||
);
|
||||
const totalCtr = activeCampaigns.reduce(
|
||||
(sum, camp) => sum + camp.metrics.ctr,
|
||||
0
|
||||
);
|
||||
const totalCpc = activeCampaigns.reduce(
|
||||
(sum, camp) => sum + camp.metrics.cpc,
|
||||
0
|
||||
);
|
||||
const numCampaigns = activeCampaigns.length;
|
||||
|
||||
setSummaryMetrics({
|
||||
totalSpend,
|
||||
totalPurchaseValue,
|
||||
totalLinkClicks,
|
||||
totalImpressions,
|
||||
totalReach,
|
||||
totalPurchases,
|
||||
avgFrequency: totalFrequency / numCampaigns,
|
||||
avgCpm: totalCpm / numCampaigns,
|
||||
avgCtr: totalCtr / numCampaigns,
|
||||
avgCpc: totalCpc / numCampaigns,
|
||||
totalPostEngagements,
|
||||
totalCampaigns: numCampaigns,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Meta Ads fetch error:", err);
|
||||
setError(`Failed to fetch Meta Ads data: ${err.message}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchMetaAdsData();
|
||||
}, [timeframe]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card className="bg-white dark:bg-gray-900">
|
||||
<CardContent className="h-[400px] flex items-center justify-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-gray-400 dark:text-gray-500" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Card className="bg-white dark:bg-gray-900">
|
||||
<CardContent className="p-4">
|
||||
<div className="text-red-500 dark:text-red-400">{error}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="h-full bg-white dark:bg-gray-900">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex justify-between items-start">
|
||||
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">
|
||||
Meta Ads Performance
|
||||
</CardTitle>
|
||||
<Select value={timeframe} onValueChange={setTimeframe}>
|
||||
<SelectTrigger className="w-36 bg-white dark:bg-gray-800">
|
||||
<SelectValue placeholder="Select range" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="today">Today</SelectItem>
|
||||
<SelectItem value="7">Last 7 days</SelectItem>
|
||||
<SelectItem value="14">Last 14 days</SelectItem>
|
||||
<SelectItem value="30">Last 30 days</SelectItem>
|
||||
<SelectItem value="90">Last 90 days</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3 mt-4">
|
||||
{[
|
||||
{
|
||||
label: "Active Campaigns",
|
||||
value: summaryMetrics?.totalCampaigns,
|
||||
},
|
||||
{
|
||||
label: "Total Spend",
|
||||
value: summaryMetrics?.totalSpend,
|
||||
options: { isMonetary: true, decimalPlaces: 0 },
|
||||
},
|
||||
{ label: "Total Reach", value: summaryMetrics?.totalReach },
|
||||
{
|
||||
label: "Total Impressions",
|
||||
value: summaryMetrics?.totalImpressions,
|
||||
},
|
||||
{
|
||||
label: "Avg Frequency",
|
||||
value: summaryMetrics?.avgFrequency,
|
||||
options: { decimalPlaces: 2 },
|
||||
},
|
||||
{
|
||||
label: "Total Engagements",
|
||||
value: summaryMetrics?.totalPostEngagements,
|
||||
},
|
||||
{
|
||||
label: "Avg CPM",
|
||||
value: summaryMetrics?.avgCpm,
|
||||
options: { isMonetary: true, decimalPlaces: 2 },
|
||||
},
|
||||
{
|
||||
label: "Avg CTR",
|
||||
value: summaryMetrics?.avgCtr,
|
||||
options: { isPercentage: true, decimalPlaces: 2 },
|
||||
},
|
||||
{
|
||||
label: "Avg CPC",
|
||||
value: summaryMetrics?.avgCpc,
|
||||
options: { isMonetary: true, decimalPlaces: 2 },
|
||||
},
|
||||
{
|
||||
label: "Total Link Clicks",
|
||||
value: summaryMetrics?.totalLinkClicks,
|
||||
},
|
||||
{ label: "Total Purchases", value: summaryMetrics?.totalPurchases },
|
||||
{
|
||||
label: "Purchase Value",
|
||||
value: summaryMetrics?.totalPurchaseValue,
|
||||
options: { isMonetary: true, decimalPlaces: 0 },
|
||||
},
|
||||
].map((card) => (
|
||||
<div key={card.label}>
|
||||
{summaryCard(card.label, card.value, card.options)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="p-4">
|
||||
<div className="grid overflow-x-auto">
|
||||
<div className="overflow-y-auto max-h-[400px]">
|
||||
<table className="min-w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200 dark:border-gray-800">
|
||||
<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">
|
||||
Campaign
|
||||
</th>
|
||||
<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">
|
||||
Spend
|
||||
</th>
|
||||
<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">
|
||||
Reach
|
||||
</th>
|
||||
<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">
|
||||
Impressions
|
||||
</th>
|
||||
<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">
|
||||
CPM
|
||||
</th>
|
||||
<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">
|
||||
CTR
|
||||
</th>
|
||||
<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">
|
||||
Results
|
||||
</th>
|
||||
<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">
|
||||
Value
|
||||
</th>
|
||||
<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">
|
||||
Engagements
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-gray-800">
|
||||
{campaigns.map((campaign) => (
|
||||
<tr
|
||||
key={campaign.id}
|
||||
className="hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
<td className="p-2 align-top">
|
||||
<div className="font-medium text-gray-900 dark:text-gray-100 break-words min-w-[200px] max-w-[300px]">
|
||||
<CampaignName name={campaign.name} />
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{campaign.objective}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<MetricCell
|
||||
value={campaign.metrics.spend}
|
||||
isMonetary
|
||||
decimalPlaces={2}
|
||||
sublabel={
|
||||
campaign.budget
|
||||
? `${formatCurrency(campaign.budget, 0)}/${
|
||||
campaign.budgetType
|
||||
}`
|
||||
: "Budget: Ad set"
|
||||
}
|
||||
/>
|
||||
|
||||
<MetricCell value={campaign.metrics.reach} />
|
||||
|
||||
<MetricCell
|
||||
value={campaign.metrics.impressions}
|
||||
label={`${campaign.metrics.frequency.toFixed(2)}x freq`}
|
||||
/>
|
||||
|
||||
<MetricCell
|
||||
value={campaign.metrics.cpm}
|
||||
isMonetary
|
||||
decimalPlaces={2}
|
||||
/>
|
||||
|
||||
<MetricCell
|
||||
value={campaign.metrics.ctr}
|
||||
isPercentage
|
||||
decimalPlaces={2}
|
||||
label={`${formatCurrency(campaign.metrics.cpc, 2)} CPC`}
|
||||
/>
|
||||
|
||||
<MetricCell
|
||||
value={getActionValue(
|
||||
campaign,
|
||||
campaign.objectiveActionType
|
||||
)}
|
||||
label={campaign.objective}
|
||||
/>
|
||||
|
||||
<MetricCell
|
||||
value={campaign.metrics.purchaseValue}
|
||||
isMonetary
|
||||
decimalPlaces={2}
|
||||
sublabel={`${formatCurrency(
|
||||
campaign.metrics.costPerResult
|
||||
)}/result`}
|
||||
/>
|
||||
|
||||
<MetricCell value={campaign.metrics.totalPostEngagements} />
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default MetaAdsOverview;
|
||||
Reference in New Issue
Block a user