Menu

Change language

GitHub repo

Ad Spend Roi Dashboard

blockmarketing

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.

Installation

$npx uipkge-ng@latest add ad-spend-roi-dashboard
Manifest:https://uipkge.dev/r/angular/ad-spend-roi-dashboard.jsonInstalls to:src/app/components/blocks/ad-spend-roi-dashboard/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

AdChannelPerformance
interface AdChannelPerformance {
  id: string
  name: string
  campaignCount: number
  spend: string
  impressions: string
  clicks: string
  ctr: string
  cpc: string
  cpa: string
  roas: string
  roasStatus: ChannelRoasStatus
  conversions: number
  revenue: string
  active: boolean
  icon: LucideIconData
  iconClass: string
}
TopCampaign
interface TopCampaign {
  id: string
  rank: number
  name: string
  channel: string
  targetNiche: string
  spend: string
  revenue: string
  cpa: string
  roas: string
}

Files installed (2)

  • src/app/components/blocks/ad-spend-roi-dashboard/ad-spend-roi-dashboard.component.ts27.6 kB
    // Multi-channel paid acquisition ad spend, blended ROAS, and CAC performance
    // dashboard: KPI cards, channel comparison table with live toggles, and top
    // campaigns. Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, Input, signal } from '@angular/core'
    import {
      Award,
      Calendar,
      Download,
      LucideAngularModule,
      Megaphone,
      Plus,
      Search,
      Share2,
      Target,
      TrendingDown,
      TrendingUp,
      Users,
      Wallet,
      Zap,
      type LucideIconData,
    } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import {
      UiCardComponent,
      UiCardContentComponent,
      UiCardDescriptionComponent,
      UiCardHeaderComponent,
      UiCardTitleComponent,
    } from '@/ui/card/card.component'
    import { UiProgressComponent } from '@/ui/progress/progress.component'
    import {
      UiSelectComponent,
      UiSelectContentComponent,
      UiSelectItemComponent,
      UiSelectTriggerComponent,
      UiSelectValueComponent,
    } from '@/ui/select/select.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    import { UiSwitchComponent } from '@/ui/switch/switch.component'
    import {
      UiTableBodyComponent,
      UiTableCellComponent,
      UiTableComponent,
      UiTableHeadComponent,
      UiTableHeaderComponent,
      UiTableRowComponent,
    } from '@/ui/table/table.component'
    
    export type ChannelRoasStatus = 'high' | 'medium' | 'low'
    
    export interface AdChannelPerformance {
      id: string
      name: string
      campaignCount: number
      spend: string
      impressions: string
      clicks: string
      ctr: string
      cpc: string
      cpa: string
      roas: string
      roasStatus: ChannelRoasStatus
      conversions: number
      revenue: string
      active: boolean
      icon: LucideIconData
      iconClass: string
    }
    
    export interface TopCampaign {
      id: string
      rank: number
      name: string
      channel: string
      targetNiche: string
      spend: string
      revenue: string
      cpa: string
      roas: string
    }
    
    const initialChannels: AdChannelPerformance[] = [
      {
        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',
      },
    ]
    
    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',
      },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-ad-spend-roi-dashboard, [ui-ad-spend-roi-dashboard]',
      standalone: true,
      // React renders the root <div> itself: the host stays out of layout and `class` goes to the root.
      host: { '[attr.class]': '"contents"' },
      imports: [
        LucideAngularModule,
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiCardDescriptionComponent,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiProgressComponent,
        UiSelectComponent,
        UiSelectContentComponent,
        UiSelectItemComponent,
        UiSelectTriggerComponent,
        UiSelectValueComponent,
        UiSeparatorComponent,
        UiSwitchComponent,
        UiTableBodyComponent,
        UiTableCellComponent,
        UiTableComponent,
        UiTableHeadComponent,
        UiTableHeaderComponent,
        UiTableRowComponent,
      ],
      template: `
        <div data-slot="ad-spend-roi-dashboard" [class]="rootClass">
          <!-- 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">
              <ui-select [value]="selectedPeriod()" (valueChange)="selectedPeriod.set($event)">
                <button ui-select-trigger class="w-56 text-xs" aria-label="Select date range">
                  <lucide-icon [img]="Calendar" class="text-muted-foreground mr-1.5 size-3.5" />
                  <ui-select-value placeholder="Select period" />
                </button>
                <ui-select-content>
                  <ui-select-item value="this-month">This Month · Aug 2026</ui-select-item>
                  <ui-select-item value="last-month">Last Month · Jul 2026</ui-select-item>
                  <ui-select-item value="last-30">Last 30 Days</ui-select-item>
                  <ui-select-item value="last-90">Last 90 Days (Q3)</ui-select-item>
                  <ui-select-item value="ytd">Year to Date · 2026</ui-select-item>
                </ui-select-content>
              </ui-select>
              <button ui-button aria-label="Download attachment" variant="outline" size="sm" class="gap-1.5 text-xs">
                <lucide-icon [img]="Download" class="size-3.5" />
                <span>Export Campaign CSV</span>
              </button>
              <button ui-button size="sm" class="gap-1.5 text-xs">
                <lucide-icon [img]="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">
            <div ui-card class="flex flex-col justify-between shadow-xs">
              <div ui-card-header 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"
                    >
                      <lucide-icon [img]="Wallet" class="size-4" />
                    </div>
                    <h3 ui-card-title class="text-muted-foreground text-sm font-medium">Total Ad Spend</h3>
                  </div>
                  <span ui-badge wrap variant="outline" class="text-xs font-normal tabular-nums">82.8% pacing</span>
                </div>
              </div>
              <div ui-card-content 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">
                  <ui-progress [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>
              </div>
            </div>
    
            <div ui-card class="flex flex-col justify-between shadow-xs">
              <div ui-card-header 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 flex size-8 shrink-0 items-center justify-center rounded-lg border shadow-xs"
                      aria-hidden="true"
                    >
                      <lucide-icon [img]="TrendingUp" class="size-4" />
                    </div>
                    <h3 ui-card-title class="text-muted-foreground text-sm font-medium">Attributed Revenue</h3>
                  </div>
                  <span
                    ui-badge
                    wrap
                    variant="outline"
                    class="border-success/20 bg-success/10 text-success text-xs font-bold tabular-nums"
                  >
                    3.39x Blended ROAS
                  </span>
                </div>
              </div>
              <div ui-card-content 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 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>
              </div>
            </div>
    
            <div ui-card class="flex flex-col justify-between shadow-xs">
              <div ui-card-header 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"
                    >
                      <lucide-icon [img]="Target" class="size-4" />
                    </div>
                    <h3 ui-card-title class="text-muted-foreground text-sm font-medium">Customer Acquisition Cost</h3>
                  </div>
                  <span
                    ui-badge
                    wrap
                    variant="outline"
                    class="border-success/20 bg-success/10 text-success gap-1 text-xs font-medium tabular-nums"
                  >
                    <lucide-icon [img]="TrendingDown" class="size-3" />
                    <span>-$6.20 vs last month</span>
                  </span>
                </div>
              </div>
              <div ui-card-content 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>
              </div>
            </div>
    
            <div ui-card class="flex flex-col justify-between shadow-xs">
              <div ui-card-header 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"
                    >
                      <lucide-icon [img]="Users" class="size-4" />
                    </div>
                    <h3 ui-card-title class="text-muted-foreground text-sm font-medium">Total Conversions</h3>
                  </div>
                  <span ui-badge wrap variant="outline" class="text-xs font-normal tabular-nums">+64 vs Jul</span>
                </div>
              </div>
              <div ui-card-content 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>
              </div>
            </div>
          </div>
    
          <!-- Channel Performance Comparison Table -->
          <div ui-card class="shadow-xs">
            <div ui-card-header>
              <div class="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
                <div>
                  <h3 ui-card-title>Channel Performance Comparison</h3>
                  <p ui-card-description>
                    Unit economics, click-through rates, acquisition volumes, and return on ad spend across channels.
                  </p>
                </div>
                <span ui-badge wrap variant="outline" class="w-fit text-xs font-normal tabular-nums">
                  4 channels active
                </span>
              </div>
            </div>
            <div ui-card-content class="p-0 sm:p-6 sm:pt-0">
              <div class="overflow-x-auto">
                <table ui-table>
                  <thead ui-table-header>
                    <tr ui-table-row>
                      <th ui-table-head class="min-w-[200px]">Channel & Campaigns</th>
                      <th ui-table-head class="text-right">Spend</th>
                      <th ui-table-head class="text-right">Impressions & Clicks (CTR)</th>
                      <th ui-table-head class="text-right">CPC & CPA</th>
                      <th ui-table-head class="text-right">ROAS</th>
                      <th ui-table-head class="text-right">Conversions & Rev</th>
                      <th ui-table-head class="text-right">Status</th>
                    </tr>
                  </thead>
                  <tbody ui-table-body>
                    @for (channel of channels(); track channel.id) {
                      <tr ui-table-row class="hover:bg-muted/50">
                        <td ui-table-cell>
                          <div class="flex items-center gap-3">
                            <div [class]="channelIconClass(channel)" aria-hidden="true">
                              <lucide-icon [img]="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>
                        </td>
                        <td ui-table-cell 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">{{ spendShare(channel) }}% of total</div>
                        </td>
                        <td ui-table-cell 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>
                        </td>
                        <td ui-table-cell 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>
                        </td>
                        <td ui-table-cell class="text-right">
                          <span ui-badge variant="outline" [class]="roasBadgeClass(channel)">
                            {{ channel.roas }} ROAS
                          </span>
                        </td>
                        <td ui-table-cell 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>
                        </td>
                        <td ui-table-cell 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>
                            <button
                              ui-switch
                              [checked]="channel.active"
                              [attr.aria-label]="'Toggle status for ' + channel.name"
                              (checkedChange)="handleToggleChannel(channel.id, $event)"
                            ></button>
                          </div>
                        </td>
                      </tr>
                    }
                  </tbody>
                </table>
              </div>
            </div>
          </div>
    
          <!-- Bottom Section: Top Performing Campaigns & Budget Allocation -->
          <div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
            <div ui-card class="shadow-xs">
              <div ui-card-header>
                <div class="flex items-center justify-between">
                  <h3 ui-card-title>Top Performing Campaigns</h3>
                  <span ui-badge wrap variant="outline" class="gap-1 text-xs">
                    <lucide-icon [img]="Award" class="text-warning size-3" />
                    <span>Highest ROAS</span>
                  </span>
                </div>
                <p ui-card-description>Ad creatives and targeting sets driving highest attribution revenue and ROAS.</p>
              </div>
              <div ui-card-content class="space-y-4">
                @for (campaign of topCampaigns; track campaign.id) {
                  <div
                    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">
                        <span
                          ui-badge
                          wrap
                          [variant]="campaign.rank === 1 ? 'default' : 'secondary'"
                          class="flex size-6 shrink-0 items-center justify-center rounded-full p-0 text-xs font-bold tabular-nums"
                        >
                          #{{ campaign.rank }}
                        </span>
                        <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">
                            <span ui-badge wrap variant="outline" class="text-xs font-normal">{{ campaign.channel }}</span>
                            <span class="text-muted-foreground text-xs">{{ campaign.targetNiche }}</span>
                          </div>
                        </div>
                      </div>
                      <span
                        ui-badge
                        wrap
                        variant="outline"
                        class="border-success/20 bg-success/10 text-success shrink-0 text-xs font-bold tabular-nums"
                      >
                        {{ campaign.roas }} ROAS
                      </span>
                    </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 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>
                }
              </div>
            </div>
    
            <div ui-card class="shadow-xs">
              <div ui-card-header>
                <div class="flex items-center justify-between">
                  <h3 ui-card-title>Budget Allocation & Guardrails</h3>
                  <span ui-badge wrap variant="outline" class="text-xs font-normal">Automated Pacing</span>
                </div>
                <p ui-card-description>
                  Live capital distribution by ad channel with automated spend-shift recommendations.
                </p>
              </div>
              <div ui-card-content class="space-y-4">
                <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-xs font-semibold tabular-nums">$5,150.00 (17.2%) remaining</p>
                  </div>
                </div>
    
                <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>
                    <ui-progress [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>
                    <ui-progress [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>
                    <ui-progress [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>
                    <ui-progress [value]="8.8" class="h-1.5 [&_[data-slot=progress-indicator]]:bg-zinc-500" />
                  </div>
                </div>
    
                <ui-separator />
    
                <div class="border-border/80 bg-muted/20 flex items-start gap-3 rounded-lg border p-3">
                  <lucide-icon [img]="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>
              </div>
            </div>
          </div>
        </div>
      `,
    })
    export class UiAdSpendRoiDashboardComponent {
      protected readonly Award = Award
      protected readonly Calendar = Calendar
      protected readonly Download = Download
      protected readonly Plus = Plus
      protected readonly Target = Target
      protected readonly TrendingDown = TrendingDown
      protected readonly TrendingUp = TrendingUp
      protected readonly Users = Users
      protected readonly Wallet = Wallet
    
      protected readonly topCampaigns = topCampaigns
    
      @Input() set initialPeriod(v: string | undefined) {
        this.selectedPeriod.set(v ?? 'this-month')
      }
      @Input('class') className?: string
    
      readonly selectedPeriod = signal('this-month')
      readonly channels = signal<AdChannelPerformance[]>(initialChannels.map((c) => ({ ...c })))
    
      get rootClass(): string {
        return cn('mx-auto w-full max-w-6xl space-y-6', this.className)
      }
    
      channelIconClass(channel: AdChannelPerformance): string {
        return cn('flex size-8 shrink-0 items-center justify-center rounded-lg border shadow-xs', channel.iconClass)
      }
    
      spendShare(channel: AdChannelPerformance): string {
        const spendNum = parseFloat(channel.spend.replace(/[$,]/g, ''))
        return ((spendNum / 24850) * 100).toFixed(1)
      }
    
      roasBadgeClass(channel: AdChannelPerformance): string {
        return 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',
        )
      }
    
      handleToggleChannel(id: string, active: boolean): void {
        this.channels.update((prev) => prev.map((ch) => (ch.id === id ? { ...ch, active } : ch)))
      }
    }
    
  • src/app/components/blocks/ad-spend-roi-dashboard/index.ts0.2 kB

Raw manifest:https://uipkge.dev/r/angular/ad-spend-roi-dashboard.json