social ads engine module screens

This commit is contained in:
Mayur Shinde
2026-06-12 13:22:03 +05:30
parent ed87e097f1
commit e53c38ce59
31 changed files with 5406 additions and 2 deletions
@@ -0,0 +1,88 @@
/**
* AudienceSegments — reusable audiences with consent count, suppression count,
* export/sync status, and refresh (spec §4, §8). Exports/syncs only permitted,
* consented, suppression-filtered contacts — the count math makes that visible.
*/
import React from 'react';
import { Users, RefreshCw, Download, Upload, ShieldCheck, ShieldX, UserCheck } from 'lucide-react';
import { useSocialAds } from '../SocialAdsContext.jsx';
import { Card, SectionHeader, Btn } from '../components/ui.jsx';
import { toast } from 'sonner';
const SEGMENT_LABEL = {
storm_geo: 'Storm geo', crm_stage: 'CRM stage', reactivation: 'Reactivation',
b2b: 'B2B', referral: 'Referral',
};
export default function AudienceSegments() {
const { audiences, refreshAudience } = useSocialAds();
const exportCsv = (a) => toast.success(`Exported ${a.consentCount.toLocaleString()} consented contacts from "${a.name}" to CSV`);
const syncAudience = (a) => toast.promise(new Promise(r => setTimeout(r, 800)), {
loading: `Syncing "${a.name}" to platform…`,
success: `Synced ${a.consentCount.toLocaleString()} contacts (suppression list applied)`,
error: 'Sync failed',
});
return (
<div>
<SectionHeader icon={Users} title="Audience Segments" description="Reusable CRM audiences. Only consented contacts sync, and suppression lists are always applied — including for retargeting." />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{audiences.map(a => {
const eligiblePct = Math.round((a.consentCount / a.totalCount) * 100);
return (
<Card key={a.id} className="!h-auto" pad="p-4">
<div className="flex items-start justify-between mb-2 gap-2">
<div className="min-w-0">
<h4 className="font-bold text-sm text-zinc-900 dark:text-white">{a.name}</h4>
<p className="text-[11px] text-zinc-400">{a.filters}</p>
</div>
<span className="text-[10px] font-bold uppercase tracking-wide px-2 py-0.5 rounded-full bg-zinc-100 dark:bg-white/10 text-zinc-500 shrink-0">
{SEGMENT_LABEL[a.segmentType] || a.segmentType}
</span>
</div>
{/* Count breakdown */}
<div className="grid grid-cols-3 gap-2 my-3">
<Stat icon={Users} label="Total" value={a.totalCount} color="text-zinc-500" />
<Stat icon={ShieldCheck} label="Consented" value={a.consentCount} color="text-emerald-500" />
<Stat icon={ShieldX} label="Suppressed" value={a.suppressionCount} color="text-red-500" />
</div>
{/* Eligible bar */}
<div className="mb-3">
<div className="flex items-center justify-between text-[10px] text-zinc-400 mb-1">
<span className="flex items-center gap-1"><UserCheck size={11} /> Eligible to sync</span>
<span className="font-mono font-semibold">{eligiblePct}%</span>
</div>
<div className="h-2 rounded-full bg-zinc-200 dark:bg-zinc-700 overflow-hidden">
<div className="h-full bg-gradient-to-r from-emerald-400 to-emerald-500" style={{ width: `${eligiblePct}%` }} />
</div>
</div>
<div className="flex flex-wrap items-center justify-between gap-2 pt-3 border-t border-zinc-100 dark:border-white/5">
<span className="text-[10px] text-zinc-400">Updated {a.lastCount}</span>
<div className="flex items-center gap-1.5 ml-auto">
<Btn size="sm" variant="ghost" icon={RefreshCw} onClick={() => refreshAudience(a.id)}>Refresh</Btn>
<Btn size="sm" variant="outline" icon={Download} onClick={() => exportCsv(a)}>CSV</Btn>
<Btn size="sm" variant="primary" icon={Upload} onClick={() => syncAudience(a)}>Sync</Btn>
</div>
</div>
</Card>
);
})}
</div>
</div>
);
}
function Stat({ icon: Icon, label, value, color }) {
return (
<div className="rounded-lg bg-zinc-50 dark:bg-white/5 p-2 text-center">
<Icon size={14} className={`${color} mx-auto mb-0.5`} />
<p className="text-sm font-mono font-bold text-zinc-800 dark:text-zinc-100">{value.toLocaleString()}</p>
<p className="text-[9px] uppercase tracking-wider text-zinc-400">{label}</p>
</div>
);
}