social ads engine module screens
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user