feat: build Campaigns list and Campaign Detail pages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-16 15:01:00 +05:30
parent 7970a34434
commit 41eadad0b3
11 changed files with 933 additions and 10 deletions

View File

@@ -1,11 +1,122 @@
import { TopBar } from "@/components/layout/top-bar";
import { useMemo, useState } from 'react';
import { Link } from 'react-router';
import { TopBar } from '@/components/layout/top-bar';
import { Tabs, TabList, Tab, TabPanel } from '@/components/application/tabs/tabs';
import { CampaignCard } from '@/components/campaigns/campaign-card';
import { useCampaigns } from '@/hooks/use-campaigns';
import { useLeads } from '@/hooks/use-leads';
import { formatCurrency } from '@/lib/format';
import type { CampaignStatus } from '@/types/entities';
type TabConfig = {
id: string;
label: string;
status: CampaignStatus | undefined;
};
const tabs: TabConfig[] = [
{ id: 'all', label: 'All', status: undefined },
{ id: 'active', label: 'Active', status: 'ACTIVE' },
{ id: 'paused', label: 'Paused', status: 'PAUSED' },
{ id: 'completed', label: 'Completed', status: 'COMPLETED' },
{ id: 'draft', label: 'Drafts', status: 'DRAFT' },
];
export const CampaignsPage = () => {
const [activeTab, setActiveTab] = useState<string>('all');
const selectedTab = tabs.find((t) => t.id === activeTab) ?? tabs[0];
const { campaigns, ads } = useCampaigns({ status: selectedTab.status });
const { campaigns: allCampaigns } = useCampaigns();
const { leads } = useLeads();
const activeCount = allCampaigns.filter((c) => c.campaignStatus === 'ACTIVE').length;
const totalSpent = allCampaigns.reduce((sum, c) => sum + (c.amountSpent?.amountMicros ?? 0), 0);
const subtitle = `${allCampaigns.length} campaigns \u00b7 ${activeCount} active \u00b7 ${formatCurrency(totalSpent)} total spend`;
// Index leads by campaignId for fast per-campaign lookups
const leadsByCampaign = useMemo(() => {
const map = new Map<string, typeof leads>();
for (const lead of leads) {
if (lead.campaignId) {
const existing = map.get(lead.campaignId);
if (existing) {
existing.push(lead);
} else {
map.set(lead.campaignId, [lead]);
}
}
}
return map;
}, [leads]);
// Index ads by campaignId
const adsByCampaign = useMemo(() => {
const map = new Map<string, typeof ads>();
for (const ad of ads) {
if (ad.campaignId) {
const existing = map.get(ad.campaignId);
if (existing) {
existing.push(ad);
} else {
map.set(ad.campaignId, [ad]);
}
}
}
return map;
}, [ads]);
// Tab badges
const tabBadges: Record<string, number> = useMemo(() => ({
all: allCampaigns.length,
active: allCampaigns.filter((c) => c.campaignStatus === 'ACTIVE').length,
paused: allCampaigns.filter((c) => c.campaignStatus === 'PAUSED').length,
completed: allCampaigns.filter((c) => c.campaignStatus === 'COMPLETED').length,
draft: allCampaigns.filter((c) => c.campaignStatus === 'DRAFT').length,
}), [allCampaigns]);
return (
<div className="flex flex-1 flex-col">
<TopBar title="Campaigns" />
<div className="flex flex-1 items-center justify-center p-8">
<p className="text-tertiary">Campaigns coming soon</p>
<TopBar title="Campaigns" subtitle={subtitle} />
<div className="flex-1 overflow-y-auto p-7 space-y-5">
<Tabs selectedKey={activeTab} onSelectionChange={(key) => setActiveTab(String(key))}>
<TabList
type="underline"
size="sm"
items={tabs.map((tab) => ({
id: tab.id,
label: tab.label,
badge: tabBadges[tab.id] > 0 ? tabBadges[tab.id] : undefined,
}))}
>
{(item) => (
<Tab key={item.id} id={item.id} label={item.label} badge={item.badge} />
)}
</TabList>
{tabs.map((tab) => (
<TabPanel key={tab.id} id={tab.id}>
<div className="mt-5 grid grid-cols-1 gap-4 xl:grid-cols-2">
{campaigns.map((campaign) => (
<Link key={campaign.id} to={`/campaigns/${campaign.id}`} className="no-underline">
<CampaignCard
campaign={campaign}
ads={adsByCampaign.get(campaign.id) ?? []}
leads={leadsByCampaign.get(campaign.id) ?? []}
/>
</Link>
))}
{campaigns.length === 0 && (
<p className="col-span-full py-12 text-center text-sm text-tertiary">
No campaigns match this filter.
</p>
)}
</div>
</TabPanel>
))}
</Tabs>
</div>
</div>
);