Skip to content

[PULSE-53] Campaigns block improvements#23

Merged
uz1mani merged 2 commits intomainfrom
staging
Feb 11, 2026
Merged

[PULSE-53] Campaigns block improvements#23
uz1mani merged 2 commits intomainfrom
staging

Conversation

@uz1mani
Copy link
Copy Markdown
Member

@uz1mani uz1mani commented Feb 11, 2026

Work Item

PULSE-53

Summary

  • Add sortable columns, source favicons, and pageviews to the Campaigns dashboard card
  • Replace loading spinner with skeleton loader; use stable row keys; improve empty value display
  • Add Campaigns export (CSV from block, PDF/Excel from main Export modal)

Changes

  • Campaigns.tsx: Sortable column headers with ChevronDownIcon; source favicons/display names via existing icon utils; pageviews column; em-dash (—) for empty Medium/Campaign; skeleton loading state; Export button for CSV download; stable keys via campaignRowKey()
  • ExportModal.tsx: campaigns?: CampaignStat[] prop; campaigns table in PDF export; campaigns sheet in Excel export; use getReferrerDisplayName for source display
  • app/sites/[id]/page.tsx: Fetch campaigns in loadData via getCampaigns(); add campaigns state; pass campaigns to ExportModal
  • CHANGELOG.md: Entry under 0.3.0-alpha for Campaigns block improvements

Test Plan

  • [] Open site dashboard, verify Campaigns card shows sortable columns and source favicons when data exists
  • Click column headers, verify sort order changes; verify chevron reflects direction
  • Verify empty Medium/Campaign show "—" instead of "-"
  • Refresh with campaigns data; verify skeleton loader during load
  • Click Export on Campaigns card; verify CSV downloads with correct data and date range in filename
  • Click main Export, choose PDF; verify campaigns table appears after Top Referrers
  • Click main Export, choose Excel; verify Campaigns sheet exists with correct columns
  • Test dark mode and keyboard focus on sort headers

@uz1mani uz1mani self-assigned this Feb 11, 2026
@uz1mani uz1mani merged commit ed0a6e2 into main Feb 11, 2026
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Feb 11, 2026

Greptile Overview

Greptile Summary

This PR enhances the Campaigns dashboard block with sortable columns, source favicons with display names, a pageviews column, and comprehensive export functionality (CSV from the block, PDF/Excel from the main Export modal). The loading state was improved from a spinner to a skeleton loader, empty values now display em-dash (—), and rows use stable keys for better React reconciliation.

Key changes:

  • Sortable column headers (Source, Medium, Campaign, Visitors, Pageviews) with chevron indicators
  • Source favicons using existing getReferrerFavicon and getReferrerDisplayName utilities, matching the Top Referrers pattern
  • Export button on Campaigns card for quick CSV download with date range in filename
  • Campaigns included in main dashboard PDF/Excel exports
  • Skeleton loading state replacing spinner for better UX

Confidence Score: 4/5

  • Safe to merge after addressing CSV export escaping
  • Well-structured feature addition with good UX improvements. One CSV injection vulnerability needs fixing in the export function, but otherwise follows existing patterns and includes proper state management.
  • Pay attention to components/dashboard/Campaigns.tsx for the CSV export escaping issue

Important Files Changed

Filename Overview
components/dashboard/Campaigns.tsx Added sortable columns, favicons, pageviews column, skeleton loading, CSV export, and stable row keys. Minor CSV injection risk in export.
components/dashboard/ExportModal.tsx Added campaigns prop and export support for PDF and Excel formats using existing patterns consistently.
app/sites/[id]/page.tsx Added campaigns data fetching with getCampaigns and passed campaigns to ExportModal. Clean integration with existing dashboard.
CHANGELOG.md Added comprehensive changelog entry describing all Campaigns block improvements accurately.

Sequence Diagram

sequenceDiagram
    participant User
    participant Page as SiteDashboardPage
    participant API as getCampaigns API
    participant Campaigns as Campaigns Component
    participant Export as ExportModal
    
    User->>Page: Load dashboard
    Page->>API: getCampaigns(siteId, dateRange, 100)
    API-->>Page: campaigns data
    Page->>Campaigns: Pass campaigns via props
    Page->>Export: Pass campaigns to ExportModal
    
    User->>Campaigns: Click column header
    Campaigns->>Campaigns: handleSort(key)
    Campaigns->>Campaigns: Update sortKey/sortDir state
    Campaigns->>Campaigns: Re-render with sortedData
    
    User->>Campaigns: Click Export button
    Campaigns->>Campaigns: handleExportCampaigns()
    Campaigns->>Campaigns: Generate CSV from sortedData
    Campaigns->>User: Download campaigns_[dateRange].csv
    
    User->>Export: Click main Export, choose PDF
    Export->>Export: Generate PDF with campaigns table
    Export->>User: Download PDF with Campaigns section
    
    User->>Export: Click main Export, choose Excel
    Export->>Export: Create Campaigns sheet
    Export->>User: Download Excel with Campaigns tab
Loading

Copy link
Copy Markdown

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +130 to +132
...rows.map(r =>
[r.source, r.medium || EMPTY_LABEL, r.campaign || EMPTY_LABEL, r.visitors, r.pageviews].join(',')
),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CSV values with special characters (, " newlines) or formula injection chars (= + - @) aren't escaped, which could break CSV parsing or create security issues

Suggested change
...rows.map(r =>
[r.source, r.medium || EMPTY_LABEL, r.campaign || EMPTY_LABEL, r.visitors, r.pageviews].join(',')
),
...rows.map(r => {
const escapeCSV = (val: string | number) => {
if (typeof val === 'number') return val.toString()
const str = val.toString()
if (str.includes(',') || str.includes('"') || str.includes('\n') || /^[=+\-@]/.test(str)) {
return `"${str.replace(/"/g, '""')}"`
}
return str
}
return [
escapeCSV(r.source),
escapeCSV(r.medium || EMPTY_LABEL),
escapeCSV(r.campaign || EMPTY_LABEL),
r.visitors,
r.pageviews
].join(',')
}),
Prompt To Fix With AI
This is a comment left during a code review.
Path: components/dashboard/Campaigns.tsx
Line: 130:132

Comment:
CSV values with special characters (`,` `"` newlines) or formula injection chars (`=` `+` `-` `@`) aren't escaped, which could break CSV parsing or create security issues

```suggestion
      ...rows.map(r => {
        const escapeCSV = (val: string | number) => {
          if (typeof val === 'number') return val.toString()
          const str = val.toString()
          if (str.includes(',') || str.includes('"') || str.includes('\n') || /^[=+\-@]/.test(str)) {
            return `"${str.replace(/"/g, '""')}"`
          }
          return str
        }
        return [
          escapeCSV(r.source),
          escapeCSV(r.medium || EMPTY_LABEL),
          escapeCSV(r.campaign || EMPTY_LABEL),
          r.visitors,
          r.pageviews
        ].join(',')
      }),
```

How can I resolve this? If you propose a fix, please make it concise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant