
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
Multi-channel paid acquisition ad spend, blended ROAS, and CAC performance dashboard: 4 primary KPI cards (total spend, revenue with blended ROAS, CAC, conversions), channel performance comparison table with live toggle switches, and top 3 performing campaign breakdown.
Also available for React ->$pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/ad-spend-roi-dashboard.json$npx shadcn-vue@latest add https://uipkge.dev/r/vue/ad-spend-roi-dashboard.json$yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/ad-spend-roi-dashboard.json$bunx shadcn-vue@latest add https://uipkge.dev/r/vue/ad-spend-roi-dashboard.jsonnpx shadcn-vue@latest add @uipkge/ad-spend-roi-dashboardInstalls to:app/components/blocks/| Name | Type / Values | Default | Required |
|---|---|---|---|
class | HTMLAttributes['class'] | — | optional |
Type aliases exported from this item's source. Use these to shape the data you pass in.
ChannelPerformanceinterface ChannelPerformance {
id: string
name: string
campaignCount: number
spend: string
impressions: string
clicks: string
ctr: string
cpc: string
cpa: string
roas: string
roasStatus: 'high' | 'medium' | 'low'
conversions: number
revenue: string
active: boolean
icon: any
iconClass: string
}TopCampaigninterface TopCampaign {
id: string
rank: number
name: string
channel: string
targetNiche: string
spend: string
revenue: string
cpa: string
roas: string
}<script setup lang="ts">
import { ref, type HTMLAttributes } from 'vue'
import {
Award,
Calendar,
Download,
Megaphone,
Plus,
Search,
Share2,
Target,
TrendingDown,
TrendingUp,
Users,
Wallet,
Zap,
} from 'lucide-vue-next'
import { cn } from '@/lib/utils'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Progress } from '@/components/ui/progress'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Separator } from '@/components/ui/separator'
import { Switch } from '@/components/ui/switch'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
const selectedPeriod = ref('this-month')
interface ChannelPerformance {
id: string
name: string
campaignCount: number
spend: string
impressions: string
clicks: string
ctr: string
cpc: string
cpa: string
roas: string
roasStatus: 'high' | 'medium' | 'low'
conversions: number
revenue: string
active: boolean
icon: any
iconClass: string
}
const channels = ref<ChannelPerformance[]>([
{
id: 'google-search',
name: 'Google Search Ads',
campaignCount: 6,
spend: '$11,200.00',
impressions: '248,500',
clicks: '14,910',
ctr: '6.00%',
cpc: '$0.75',
cpa: '$42.10',
roas: '4.2x',
roasStatus: 'high',
conversions: 266,
revenue: '$47,040.00',
active: true,
icon: Search,
iconClass: 'bg-info/10 text-info border-info/20',
},
{
id: 'meta-ads',
name: 'Meta / Instagram Ads',
campaignCount: 8,
spend: '$7,650.00',
impressions: '420,000',
clicks: '8,400',
ctr: '2.00%',
cpc: '$0.91',
cpa: '$49.35',
roas: '3.1x',
roasStatus: 'high',
conversions: 155,
revenue: '$23,715.00',
active: true,
icon: Share2,
iconClass: 'bg-chart-1/10 text-chart-1 border-chart-1/20',
},
{
id: 'linkedin-ads',
name: 'LinkedIn Sponsored',
campaignCount: 3,
spend: '$3,800.00',
impressions: '54,000',
clicks: '1,080',
ctr: '2.00%',
cpc: '$3.52',
cpa: '$64.40',
roas: '2.4x',
roasStatus: 'medium',
conversions: 59,
revenue: '$9,120.00',
active: true,
icon: Megaphone,
iconClass: 'bg-info/10 text-info border-info/20',
},
{
id: 'twitter-ads',
name: 'Twitter / X Ads',
campaignCount: 2,
spend: '$2,200.00',
impressions: '110,000',
clicks: '1,650',
ctr: '1.50%',
cpc: '$1.33',
cpa: '$68.75',
roas: '1.8x',
roasStatus: 'low',
conversions: 32,
revenue: '$3,960.00',
active: false,
icon: Zap,
iconClass: 'bg-zinc-500/10 text-zinc-600 text-muted-foreground border-zinc-500/20',
},
])
interface TopCampaign {
id: string
rank: number
name: string
channel: string
targetNiche: string
spend: string
revenue: string
cpa: string
roas: string
}
const topCampaigns: TopCampaign[] = [
{
id: 'camp-1',
rank: 1,
name: 'Q3 Retargeting · High-Intent Abandoned Cart',
channel: 'Google Search Ads',
targetNiche: 'SaaS Mid-Market Decision Makers',
spend: '$4,120.00',
revenue: '$22,248.00',
cpa: '$34.33',
roas: '5.4x',
},
{
id: 'camp-2',
rank: 2,
name: 'Competitor Keyword Conquesting - Core US',
channel: 'Google Search Ads',
targetNiche: 'Direct B2B Search Intent',
spend: '$6,800.00',
revenue: '$31,280.00',
cpa: '$38.20',
roas: '4.6x',
},
{
id: 'camp-3',
rank: 3,
name: 'Founder Video Testimonial - Lookalike 1%',
channel: 'Meta / Instagram Ads',
targetNiche: 'Seed & Series A Tech Founders',
spend: '$3,450.00',
revenue: '$13,110.00',
cpa: '$46.00',
roas: '3.8x',
},
]
</script>
<template>
<div data-slot="ad-spend-roi-dashboard" :class="cn('mx-auto w-full max-w-6xl space-y-6', props.class)">
<!-- Header -->
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div class="space-y-1">
<h1 class="text-foreground text-2xl font-bold tracking-tight sm:text-3xl">Paid Acquisition & Ad Spend ROI</h1>
<p class="text-muted-foreground text-sm">
Multi-channel paid acquisition ad spend, blended ROAS, and CAC performance dashboard.
</p>
</div>
<div class="flex flex-wrap items-center gap-2.5">
<Select v-model="selectedPeriod">
<SelectTrigger class="w-56 text-xs" aria-label="Select date range">
<Calendar class="text-muted-foreground mr-1.5 size-3.5" />
<SelectValue placeholder="Select period" />
</SelectTrigger>
<SelectContent>
<SelectItem value="this-month">This Month · Aug 2026</SelectItem>
<SelectItem value="last-month">Last Month · Jul 2026</SelectItem>
<SelectItem value="last-30">Last 30 Days</SelectItem>
<SelectItem value="last-90">Last 90 Days (Q3)</SelectItem>
<SelectItem value="ytd">Year to Date · 2026</SelectItem>
</SelectContent>
</Select>
<Button aria-label="Download attachment" variant="outline" size="sm" class="gap-1.5 text-xs">
<Download class="size-3.5" />
<span>Export Campaign CSV</span>
</Button>
<Button size="sm" class="gap-1.5 text-xs">
<Plus class="size-3.5" />
<span>Connect Ad Account</span>
</Button>
</div>
</div>
<!-- 4 Primary Performance KPI Cards -->
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<!-- Total Ad Spend -->
<Card class="flex flex-col justify-between shadow-xs">
<CardHeader class="pb-2">
<div class="flex flex-wrap items-center justify-between gap-2">
<div class="flex min-w-0 items-center gap-2">
<div
class="border-border bg-muted text-foreground flex size-8 shrink-0 items-center justify-center rounded-lg border shadow-xs"
aria-hidden="true"
>
<Wallet class="size-4" />
</div>
<CardTitle class="text-muted-foreground text-sm font-medium">Total Ad Spend</CardTitle>
</div>
<Badge wrap variant="outline" class="text-xs font-normal tabular-nums">82.8% pacing</Badge>
</div>
</CardHeader>
<CardContent class="space-y-3">
<div>
<div class="flex items-baseline gap-1.5">
<span class="text-foreground text-2xl font-bold tracking-tight tabular-nums">$24,850.00</span>
</div>
<p class="text-muted-foreground mt-0.5 text-xs tabular-nums">Monthly budget $30,000.00</p>
</div>
<div class="space-y-1.5">
<Progress :model-value="82.8" class="[&_[data-slot=progress-indicator]]:bg-primary h-1.5" />
<div class="text-muted-foreground flex items-center justify-between text-xs tabular-nums">
<span>$5,150.00 headroom</span>
<span>10 days left</span>
</div>
</div>
</CardContent>
</Card>
<!-- Attributed Revenue -->
<Card class="flex flex-col justify-between shadow-xs">
<CardHeader class="pb-2">
<div class="flex flex-wrap items-center justify-between gap-2">
<div class="flex min-w-0 items-center gap-2">
<div
class="border-success/20 bg-success/10 text-success text-success flex size-8 shrink-0 items-center justify-center rounded-lg border shadow-xs"
aria-hidden="true"
>
<TrendingUp class="size-4" />
</div>
<CardTitle class="text-muted-foreground text-sm font-medium">Attributed Revenue</CardTitle>
</div>
<Badge
wrap
variant="outline"
class="border-success/20 bg-success/10 text-success text-success text-xs font-bold tabular-nums"
>
3.39x Blended ROAS
</Badge>
</div>
</CardHeader>
<CardContent class="space-y-3">
<div>
<div class="flex items-baseline gap-1.5">
<span class="text-foreground text-2xl font-bold tracking-tight tabular-nums">$84,200.00</span>
</div>
<p class="text-muted-foreground mt-0.5 text-xs">Direct ad touch attribution</p>
</div>
<div class="border-border/60 border-t pt-2 text-xs">
<div class="flex items-center justify-between">
<span class="text-muted-foreground">Period growth:</span>
<span class="text-success text-success font-semibold tabular-nums">+$18,450.00 (+28.1%)</span>
</div>
<p class="text-muted-foreground mt-0.5 text-xs">Target ROAS: 2.80x (exceeded)</p>
</div>
</CardContent>
</Card>
<!-- Customer Acquisition Cost (CAC) -->
<Card class="flex flex-col justify-between shadow-xs">
<CardHeader class="pb-2">
<div class="flex flex-wrap items-center justify-between gap-2">
<div class="flex min-w-0 items-center gap-2">
<div
class="border-border bg-muted text-foreground flex size-8 shrink-0 items-center justify-center rounded-lg border shadow-xs"
aria-hidden="true"
>
<Target class="size-4" />
</div>
<CardTitle class="text-muted-foreground text-sm font-medium">Customer Acquisition Cost</CardTitle>
</div>
<Badge
wrap
variant="outline"
class="border-success/20 bg-success/10 text-success text-success gap-1 text-xs font-medium tabular-nums"
>
<TrendingDown class="size-3" />
<span>-$6.20 vs last month</span>
</Badge>
</div>
</CardHeader>
<CardContent class="space-y-3">
<div>
<div class="flex items-baseline gap-1.5">
<span class="text-foreground text-2xl font-bold tracking-tight tabular-nums">$48.50</span>
<span class="text-muted-foreground text-xs">/ customer</span>
</div>
<p class="text-muted-foreground mt-0.5 text-xs">Blended across all 4 channels</p>
</div>
<div class="border-border/60 border-t pt-2 text-xs">
<div class="flex items-center justify-between">
<span class="text-muted-foreground">Target CAC ceiling:</span>
<span class="text-foreground font-semibold tabular-nums">$55.00 max</span>
</div>
<p class="text-success mt-0.5 text-xs font-medium">11.8% below budget cap</p>
</div>
</CardContent>
</Card>
<!-- Total Conversions -->
<Card class="flex flex-col justify-between shadow-xs">
<CardHeader class="pb-2">
<div class="flex flex-wrap items-center justify-between gap-2">
<div class="flex min-w-0 items-center gap-2">
<div
class="border-border bg-muted text-foreground flex size-8 shrink-0 items-center justify-center rounded-lg border shadow-xs"
aria-hidden="true"
>
<Users class="size-4" />
</div>
<CardTitle class="text-muted-foreground text-sm font-medium">Total Conversions</CardTitle>
</div>
<Badge wrap variant="outline" class="text-xs font-normal tabular-nums">+64 vs Jul</Badge>
</div>
</CardHeader>
<CardContent class="space-y-3">
<div>
<div class="flex items-baseline gap-1.5">
<span class="text-foreground text-2xl font-bold tracking-tight tabular-nums">512</span>
<span class="text-foreground text-sm font-semibold">Paid Customers</span>
</div>
<p class="text-muted-foreground mt-0.5 text-xs tabular-nums">Avg order value $164.45</p>
</div>
<div class="border-border/60 border-t pt-2 text-xs">
<div class="flex items-center justify-between">
<span class="text-muted-foreground">Blended Conversion Rate:</span>
<span class="text-foreground font-semibold tabular-nums">14.2%</span>
</div>
<p class="text-muted-foreground mt-0.5 text-xs">Organic lift multiplier: 1.18x</p>
</div>
</CardContent>
</Card>
</div>
<!-- Channel Performance Comparison Table -->
<Card class="shadow-xs">
<CardHeader>
<div class="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
<div>
<CardTitle>Channel Performance Comparison</CardTitle>
<CardDescription>
Unit economics, click-through rates, acquisition volumes, and return on ad spend across channels.
</CardDescription>
</div>
<Badge wrap variant="outline" class="w-fit text-xs font-normal tabular-nums">4 channels active</Badge>
</div>
</CardHeader>
<CardContent class="p-0 sm:p-6 sm:pt-0">
<div class="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead class="min-w-[200px]">Channel & Campaigns</TableHead>
<TableHead class="text-right">Spend</TableHead>
<TableHead class="text-right">Impressions & Clicks (CTR)</TableHead>
<TableHead class="text-right">CPC & CPA</TableHead>
<TableHead class="text-right">ROAS</TableHead>
<TableHead class="text-right">Conversions & Rev</TableHead>
<TableHead class="text-right">Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="channel in channels" :key="channel.id" class="hover:bg-muted/50">
<TableCell>
<div class="flex items-center gap-3">
<div
:class="
cn(
'flex size-8 shrink-0 items-center justify-center rounded-lg border shadow-xs',
channel.iconClass,
)
"
aria-hidden="true"
>
<component :is="channel.icon" class="size-4" />
</div>
<div>
<div class="text-foreground text-sm font-semibold">{{ channel.name }}</div>
<div class="text-muted-foreground text-xs tabular-nums">
{{ channel.campaignCount }} active campaigns
</div>
</div>
</div>
</TableCell>
<TableCell class="text-right">
<div class="text-foreground text-sm font-semibold tabular-nums">{{ channel.spend }}</div>
<div class="text-muted-foreground text-xs tabular-nums">
{{ ((parseFloat(channel.spend.replace(/[$,]/g, '')) / 24850) * 100).toFixed(1) }}% of total
</div>
</TableCell>
<TableCell class="text-right">
<div class="text-foreground text-xs font-medium tabular-nums">{{ channel.impressions }} imp</div>
<div class="text-muted-foreground text-xs tabular-nums">
{{ channel.clicks }} clicks ({{ channel.ctr }})
</div>
</TableCell>
<TableCell class="text-right">
<div class="text-foreground text-xs font-semibold tabular-nums">{{ channel.cpc }} CPC</div>
<div class="text-muted-foreground text-xs tabular-nums">{{ channel.cpa }} CPA</div>
</TableCell>
<TableCell class="text-right">
<Badge
wrap
variant="outline"
:class="
cn(
'text-xs font-bold tabular-nums',
channel.roasStatus === 'high' && 'border-success/20 bg-success/10 text-success',
channel.roasStatus === 'medium' && 'border-info/20 bg-info/10 text-info',
channel.roasStatus === 'low' && 'border-warning/20 bg-warning/10 text-warning',
)
"
>
{{ channel.roas }} ROAS
</Badge>
</TableCell>
<TableCell class="text-right">
<div class="text-foreground text-xs font-bold tabular-nums">{{ channel.revenue }}</div>
<div class="text-muted-foreground text-xs tabular-nums">{{ channel.conversions }} conversions</div>
</TableCell>
<TableCell class="text-right">
<div class="flex items-center justify-end gap-2">
<span class="text-muted-foreground text-xs font-medium">
{{ channel.active ? 'Active' : 'Paused' }}
</span>
<Switch v-model="channel.active" :aria-label="'Toggle status for ' + channel.name" />
</div>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</CardContent>
</Card>
<!-- Bottom Section: Top Performing Campaigns & Budget Allocation -->
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
<!-- Top Performing Campaigns Breakdown Card -->
<Card class="shadow-xs">
<CardHeader>
<div class="flex items-center justify-between">
<CardTitle>Top Performing Campaigns</CardTitle>
<Badge wrap variant="outline" class="gap-1 text-xs">
<Award class="text-warning size-3" />
<span>Highest ROAS</span>
</Badge>
</div>
<CardDescription>
Ad creatives and targeting sets driving highest attribution revenue and ROAS.
</CardDescription>
</CardHeader>
<CardContent class="space-y-4">
<div
v-for="campaign in topCampaigns"
:key="campaign.id"
class="border-border bg-muted/30 hover:bg-muted/60 flex flex-col gap-3 rounded-lg border p-4 transition-colors"
>
<div class="flex items-start justify-between gap-2">
<div class="flex items-start gap-2.5">
<Badge
:variant="campaign.rank === 1 ? 'default' : 'secondary'"
class="flex size-6 shrink-0 items-center justify-center rounded-full p-0 text-xs font-bold whitespace-normal tabular-nums"
>
#{{ campaign.rank }}
</Badge>
<div class="space-y-0.5">
<h4 class="text-foreground text-sm font-semibold">{{ campaign.name }}</h4>
<div class="flex flex-wrap items-center gap-1.5">
<Badge wrap variant="outline" class="text-xs font-normal">
{{ campaign.channel }}
</Badge>
<span class="text-muted-foreground text-xs">{{ campaign.targetNiche }}</span>
</div>
</div>
</div>
<Badge
wrap
variant="outline"
class="border-success/20 bg-success/10 text-success text-success shrink-0 text-xs font-bold tabular-nums"
>
{{ campaign.roas }} ROAS
</Badge>
</div>
<div class="border-border/60 grid grid-cols-1 gap-2 border-t pt-2 text-xs sm:grid-cols-3">
<div>
<span class="text-muted-foreground">Spend:</span>
<p class="text-foreground font-semibold tabular-nums">{{ campaign.spend }}</p>
</div>
<div>
<span class="text-muted-foreground">Attributed Rev:</span>
<p class="text-success text-success font-semibold tabular-nums">{{ campaign.revenue }}</p>
</div>
<div class="sm:text-right">
<span class="text-muted-foreground">Unit CPA:</span>
<p class="text-foreground font-semibold tabular-nums">{{ campaign.cpa }}</p>
</div>
</div>
</div>
</CardContent>
</Card>
<!-- Budget Allocation & Channel Efficiency Card -->
<Card class="shadow-xs">
<CardHeader>
<div class="flex items-center justify-between">
<CardTitle>Budget Allocation & Guardrails</CardTitle>
<Badge wrap variant="outline" class="text-xs font-normal">Automated Pacing</Badge>
</div>
<CardDescription>
Live capital distribution by ad channel with automated spend-shift recommendations.
</CardDescription>
</CardHeader>
<CardContent class="space-y-4">
<!-- Spend pacing summary box -->
<div
class="border-border bg-muted/40 flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5 rounded-lg border p-4"
>
<div>
<p class="text-muted-foreground text-xs font-medium tracking-wider uppercase">Total Deployed Spend</p>
<p class="text-foreground text-3xl font-bold tracking-tight tabular-nums">$24,850.00</p>
</div>
<div class="text-right">
<span class="text-muted-foreground text-xs">
Budget Cap: <strong class="text-foreground font-semibold tabular-nums">$30,000.00</strong>
</span>
<p class="text-success text-success text-xs font-semibold tabular-nums">$5,150.00 (17.2%) remaining</p>
</div>
</div>
<!-- Channel Share Progress Bars -->
<div class="space-y-3 text-xs">
<div class="space-y-1">
<div class="flex items-center justify-between">
<span class="text-foreground font-medium">Google Search Ads (45.1%)</span>
<span class="text-foreground font-semibold tabular-nums">$11,200.00 / 4.2x ROAS</span>
</div>
<Progress :model-value="45.1" class="[&_[data-slot=progress-indicator]]:bg-info h-1.5" />
</div>
<div class="space-y-1">
<div class="flex items-center justify-between">
<span class="text-foreground font-medium">Meta / Instagram Ads (30.8%)</span>
<span class="text-foreground font-semibold tabular-nums">$7,650.00 / 3.1x ROAS</span>
</div>
<Progress :model-value="30.8" class="[&_[data-slot=progress-indicator]]:bg-chart-1 h-1.5" />
</div>
<div class="space-y-1">
<div class="flex items-center justify-between">
<span class="text-foreground font-medium">LinkedIn Sponsored (15.3%)</span>
<span class="text-foreground font-semibold tabular-nums">$3,800.00 / 2.4x ROAS</span>
</div>
<Progress :model-value="15.3" class="[&_[data-slot=progress-indicator]]:bg-info h-1.5" />
</div>
<div class="space-y-1">
<div class="flex items-center justify-between">
<span class="text-foreground font-medium">Twitter / X Ads (8.8%)</span>
<span class="text-muted-foreground font-semibold tabular-nums">$2,200.00 / 1.8x ROAS</span>
</div>
<Progress :model-value="8.8" class="h-1.5 [&_[data-slot=progress-indicator]]:bg-zinc-500" />
</div>
</div>
<Separator />
<!-- Growth Strategy Note -->
<div class="border-border/80 bg-muted/20 flex items-start gap-3 rounded-lg border p-3">
<TrendingUp class="text-primary mt-0.5 size-4 shrink-0" />
<div class="space-y-1">
<h5 class="text-foreground text-xs font-semibold">Growth Optimization Insight</h5>
<p class="text-muted-foreground text-xs leading-relaxed">
Google Search & Meta campaigns are outperforming blended ROAS target (+0.59x). Pausing Twitter / X Ads
saves $73.30/day to reallocate into the highest-converting Q3 Retargeting set.
</p>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
</template>
Raw manifest:https://uipkge.dev/r/vue/ad-spend-roi-dashboard.json