From 050f13168cca7772971d54b812a67f23d523827a Mon Sep 17 00:00:00 2001 From: Ian Krieger <48930920+IanKrieger@users.noreply.github.com> Date: Thu, 25 Jan 2024 12:54:38 -0500 Subject: [PATCH] feat: add viewthrough and clickthrough reporting (#1023) * feat: add viewthrough and clickthrough reporting * test: should only be 2 decimal places * fix: update overview to convert from bignumber * fix: remove filter from change * fix: future code * fix: make sure that overview are numbers * fix: make sure we have maximum digits * fix: rebase deleted file --- src/components/Datagrid/renderers.tsx | 14 +- src/graphql/analytics-overview.generated.tsx | 49 ++++- src/graphql/analytics-overview.graphql | 13 +- src/user/adSet/AdSetList.tsx | 12 +- src/user/ads/AdList.tsx | 12 +- .../analyticsOverview/components/LiveFeed.tsx | 33 ++- .../components/MetricFilter.tsx | 3 +- .../components/MetricSelect.tsx | 12 + .../analyticsOverview/lib/ads.library.ts | 4 + .../analyticsOverview/lib/creative.library.ts | 4 + .../analyticsOverview/lib/library.spec.ts | 208 ++++++++++++------ .../analyticsOverview/lib/os.library.ts | 10 +- .../analyticsOverview/lib/overview.library.ts | 86 ++++---- .../reports/campaign/EngagementsOverview.tsx | 18 +- .../analyticsOverview/types/index.ts | 34 +-- src/user/analytics/renderers/index.tsx | 11 +- src/user/campaignList/CampaignList.tsx | 14 +- src/user/views/user/AdDetailTable.tsx | 31 ++- src/util/bignumber.ts | 10 + 19 files changed, 367 insertions(+), 211 deletions(-) create mode 100644 src/util/bignumber.ts diff --git a/src/components/Datagrid/renderers.tsx b/src/components/Datagrid/renderers.tsx index 78efd319..e770a212 100644 --- a/src/components/Datagrid/renderers.tsx +++ b/src/components/Datagrid/renderers.tsx @@ -17,6 +17,8 @@ import { CampaignExtras } from "user/adSet/AdSetList"; import { FilterContext } from "state/context"; import { refetchAdvertiserCampaignsQuery } from "graphql/advertiser.generated"; import { UpdateAdSetInput } from "graphql/types"; +import { toLocaleString } from "util/bignumber"; +import BigNumber from "bignumber.js"; export type CellValueRenderer = (value: any) => ReactNode; const ADS_DEFAULT_TIMEZONE = "America/New_York"; @@ -78,16 +80,14 @@ export const StandardRenderers: Record = { }; export function renderMonetaryAmount( - value: number, + value: BigNumber | number, currency: string, -): ReactNode { +) { + const val = BigNumber(value); if (currency === "USD") { - return `$${value.toLocaleString("en", { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - })}`; + return `$${toLocaleString(val)}`; } else { - return {value.toLocaleString("en")} BAT; + return `${toLocaleString(val)} ${currency}`; } } diff --git a/src/graphql/analytics-overview.generated.tsx b/src/graphql/analytics-overview.generated.tsx index 5719f678..c53f9a70 100644 --- a/src/graphql/analytics-overview.generated.tsx +++ b/src/graphql/analytics-overview.generated.tsx @@ -14,9 +14,17 @@ export type EngagementFragment = { creativeid: string; creativestate: string; creativepayload: string; - count: number; + view: string; + click: string; + viewthroughConversion: string; + clickthroughConversion: string; + conversion: string; + dismiss: string; + downvote: string; + landed: string; + spend: string; + upvote: string; price: number; - cost: number; android: number; ios: number; linux: number; @@ -51,9 +59,17 @@ export type CampaignWithEngagementsFragment = { creativeid: string; creativestate: string; creativepayload: string; - count: number; + view: string; + click: string; + viewthroughConversion: string; + clickthroughConversion: string; + conversion: string; + dismiss: string; + downvote: string; + landed: string; + spend: string; + upvote: string; price: number; - cost: number; android: number; ios: number; linux: number; @@ -94,9 +110,17 @@ export type AnalyticOverviewQuery = { creativeid: string; creativestate: string; creativepayload: string; - count: number; + view: string; + click: string; + viewthroughConversion: string; + clickthroughConversion: string; + conversion: string; + dismiss: string; + downvote: string; + landed: string; + spend: string; + upvote: string; price: number; - cost: number; android: number; ios: number; linux: number; @@ -134,9 +158,18 @@ export const EngagementFragmentDoc = gql` creativeid creativestate creativepayload - count + view + click + viewthroughConversion + clickthroughConversion + conversion + dismiss + downvote + landed + spend + upvote + downvote price - cost android ios linux diff --git a/src/graphql/analytics-overview.graphql b/src/graphql/analytics-overview.graphql index 45190b1a..85ab0ba4 100644 --- a/src/graphql/analytics-overview.graphql +++ b/src/graphql/analytics-overview.graphql @@ -9,9 +9,18 @@ fragment Engagement on Engagement { creativeid creativestate creativepayload - count + view + click + viewthroughConversion + clickthroughConversion + conversion + dismiss + downvote + landed + spend + upvote + downvote price - cost android ios linux diff --git a/src/user/adSet/AdSetList.tsx b/src/user/adSet/AdSetList.tsx index bdb2031e..5a793ad6 100644 --- a/src/user/adSet/AdSetList.tsx +++ b/src/user/adSet/AdSetList.tsx @@ -1,10 +1,7 @@ import { Chip } from "@mui/material"; import { Status } from "components/Campaigns/Status"; import _ from "lodash"; -import { - adSetOnOffState, - StandardRenderers, -} from "components/Datagrid/renderers"; +import { adSetOnOffState } from "components/Datagrid/renderers"; import { CampaignAdsFragment } from "graphql/campaign.generated"; import { CampaignSource } from "graphql/types"; import { StatsMetric } from "user/analytics/analyticsOverview/types"; @@ -85,13 +82,6 @@ export function AdSetList({ campaign, loading, engagements }: Props) { filterable: false, width: 100, }, - { - field: "createdAt", - headerName: "Created", - valueGetter: ({ row }) => row.createdAt, - renderCell: ({ row }) => StandardRenderers.date(row.createdAt), - width: 120, - }, { field: "name", headerName: "Name", diff --git a/src/user/ads/AdList.tsx b/src/user/ads/AdList.tsx index 8b33c476..d1eb2a06 100644 --- a/src/user/ads/AdList.tsx +++ b/src/user/ads/AdList.tsx @@ -7,7 +7,6 @@ import { StatsMetric } from "user/analytics/analyticsOverview/types"; import { AdDetailTable } from "user/views/user/AdDetailTable"; import { GridColDef } from "@mui/x-data-grid"; import { CreativeFragment } from "graphql/creative.generated"; -import { StandardRenderers } from "components/Datagrid/renderers"; import { Box } from "@mui/material"; interface Props { @@ -48,16 +47,13 @@ export function AdList({ campaign, loading, engagements }: Props) { const ads: AdDetails[] = _.flatMap(adSets, "ads"); const columns: GridColDef[] = [ - { - field: "createdAt", - headerName: "Created", - valueGetter: ({ row }) => row.creative.createdAt, - renderCell: ({ row }) => StandardRenderers.date(row.creative.createdAt), - }, { field: "name", headerName: "Ad Name", - valueGetter: ({ row }) => row.creative.name, + valueGetter: ({ row }) => + row.adState !== "deleted" + ? row.creative.name + : `(DELETED) ${row.creative.name}`, renderCell: ({ row }) => ( {row.adState === "deleted" && (DELETED) } diff --git a/src/user/analytics/analyticsOverview/components/LiveFeed.tsx b/src/user/analytics/analyticsOverview/components/LiveFeed.tsx index b6a69349..273a14e9 100644 --- a/src/user/analytics/analyticsOverview/components/LiveFeed.tsx +++ b/src/user/analytics/analyticsOverview/components/LiveFeed.tsx @@ -1,5 +1,6 @@ import { Box, Chip, Typography } from "@mui/material"; import { OverviewDetail, StatsMetric } from "../types"; +import { toLocaleString } from "util/bignumber"; interface OverviewProps extends OverviewDetail { currency: string; @@ -18,46 +19,54 @@ interface Feed { export default function LiveFeed({ overview, processed }: LiveFeedProps) { const { budget, currency } = overview; - const realSpend = processed.spend > budget ? budget : processed.spend; + const realSpend = processed.spend.gte(budget) ? budget : processed.spend; const feedValues: Feed[] = [ { label: "Click-through rate", - value: `${processed.ctr.toFixed(2)}%`, + value: `${toLocaleString(processed.ctr)}%`, }, { label: "Site visit rate", - value: `${processed.visitRate.toFixed(2)}%`, + value: `${toLocaleString(processed.visitRate)}%`, }, { label: "Dismissal rate", - value: `${processed.dismissRate.toFixed(2)}%`, + value: `${toLocaleString(processed.dismissRate)}%`, }, { label: "Click to site visit rate", - value: `${processed.landingRate.toFixed(2)}%`, + value: `${toLocaleString(processed.landingRate)}%`, }, - { label: "Upvotes", value: `${processed.upvotes}` }, - { label: "Downvotes", value: `${processed.downvotes}` }, + { label: "Upvotes", value: toLocaleString(processed.upvotes) }, + { label: "Downvotes", value: toLocaleString(processed.downvotes) }, { label: "Spend", - value: `${realSpend.toLocaleString()} ${currency}`, + value: `$${toLocaleString(realSpend)} ${currency}`, }, { label: "Budget", - value: `${budget.toLocaleString()} ${currency}`, + value: `$${toLocaleString(budget)} ${currency}`, }, ]; - if (processed.conversions > 0) { + if (processed.conversions.gt(0)) { feedValues.push( { label: "Conversions", - value: `${processed.conversions}`, + value: toLocaleString(processed.conversions), + }, + { + label: "View-through Conversions", + value: toLocaleString(processed.viewthroughConversion), + }, + { + label: "Click-through Conversions", + value: toLocaleString(processed.clickthroughConversion), }, { label: "CPA", - value: `$${processed.cpa.toLocaleString()}`, + value: `$${toLocaleString(processed.cpa)} ${currency}`, }, ); } diff --git a/src/user/analytics/analyticsOverview/components/MetricFilter.tsx b/src/user/analytics/analyticsOverview/components/MetricFilter.tsx index a3a37a98..7ca69300 100644 --- a/src/user/analytics/analyticsOverview/components/MetricFilter.tsx +++ b/src/user/analytics/analyticsOverview/components/MetricFilter.tsx @@ -2,6 +2,7 @@ import MetricSelect from "user/analytics/analyticsOverview/components/MetricSele import { Box, Stack, Switch, Tooltip, Typography } from "@mui/material"; import { decideValueAttribute } from "user/analytics/analyticsOverview/lib/overview.library"; import { Metrics, StatsMetric } from "user/analytics/analyticsOverview/types"; +import { toLocaleString } from "util/bignumber"; type FilterMetric = { key: keyof StatsMetric; @@ -56,7 +57,7 @@ const FilterBox = ({ width="100%" > - {attrs.prefix ?? ""} {displayVal} {attrs.suffix ?? ""} + {attrs.prefix ?? ""} {toLocaleString(displayVal)} {attrs.suffix ?? ""} { processed.conversion.macos, ); - expect(landed).toBe(300); - expect(ctr).toBe(4); + expect(landed.toNumber()).toBe(300); + expect(ctr.toNumber()).toBe(4); const fixed = cpa.toPrecision(2); expect(fixed).toBe("0.83"); }); @@ -234,44 +304,48 @@ it("should calculate metrics per creative id", () => { expect(creatives).toMatchInlineSnapshot(` [ { - "clicks": 2, - "convRate": 150, - "conversions": 3, - "cpa": 0, + "clicks": "2", + "clickthroughConversion": "1", + "convRate": "150", + "conversions": "3", + "cpa": "0", "creativePayload": { "body": "be cool", "title": "name one", }, - "ctr": 5.405405405405405, - "dismissRate": 0, - "dismissals": 0, - "downvotes": 0, - "landingRate": 100, - "landings": 2, - "spend": 0, - "upvotes": 0, - "views": 37, - "visitRate": 5.405405405405405, + "ctr": "3.226", + "dismissRate": "0", + "dismissals": "0", + "downvotes": "0", + "landingRate": "100", + "landings": "2", + "spend": "0", + "upvotes": "0", + "views": "62", + "viewthroughConversion": "2", + "visitRate": "3.226", }, { - "clicks": 2, - "convRate": 0, - "conversions": 0, - "cpa": NaN, + "clicks": "2", + "clickthroughConversion": "0", + "convRate": "0", + "conversions": "0", + "cpa": "0", "creativePayload": { "body": "be uncool", "title": "name two", }, - "ctr": 20, - "dismissRate": 0, - "dismissals": 0, - "downvotes": 0, - "landingRate": 150, - "landings": 3, - "spend": 0, - "upvotes": 0, - "views": 10, - "visitRate": 30, + "ctr": "20", + "dismissRate": "0", + "dismissals": "0", + "downvotes": "0", + "landingRate": "150", + "landings": "3", + "spend": "0", + "upvotes": "0", + "views": "10", + "viewthroughConversion": "0", + "visitRate": "30", }, ] `); @@ -281,20 +355,22 @@ it("should calculate overall stats", () => { const stats = processStats(engagements); expect(stats).toMatchInlineSnapshot(` { - "clicks": 4, - "convRate": 75, - "conversions": 3, - "cpa": 0, - "ctr": 8.51063829787234, - "dismissRate": 0, - "dismissals": 0, - "downvotes": 0, - "landingRate": 125, - "landings": 5, - "spend": 0, - "upvotes": 0, - "views": 47, - "visitRate": 10.638297872340425, + "clicks": "4", + "clickthroughConversion": "1", + "convRate": "75", + "conversions": "3", + "cpa": "0", + "ctr": "5.556", + "dismissRate": "0", + "dismissals": "0", + "downvotes": "0", + "landingRate": "125", + "landings": "5", + "spend": "0", + "upvotes": "0", + "views": "72", + "viewthroughConversion": "2", + "visitRate": "6.944", } `); }); @@ -313,7 +389,7 @@ it("should calculate specific time series data by time range", () => { "metric1DataSet": [ [ 1677657600000, - 47, + 72, ], ], "metric2DataSet": [ diff --git a/src/user/analytics/analyticsOverview/lib/os.library.ts b/src/user/analytics/analyticsOverview/lib/os.library.ts index 24599836..49580384 100644 --- a/src/user/analytics/analyticsOverview/lib/os.library.ts +++ b/src/user/analytics/analyticsOverview/lib/os.library.ts @@ -52,11 +52,11 @@ export const mapOsStats = (stats: OSMetric) => { const calculateForOS = (n: OS, d: OS, isPercent: boolean = true) => { return { - android: calculateMetric(isPercent, n.android, d.android), - ios: calculateMetric(isPercent, n.ios, d.ios), - windows: calculateMetric(isPercent, n.windows, d.windows), - linux: calculateMetric(isPercent, n.linux, d.linux), - macos: calculateMetric(isPercent, n.macos, d.macos), + android: calculateMetric(isPercent, n.android, d.android).toNumber(), + ios: calculateMetric(isPercent, n.ios, d.ios).toNumber(), + windows: calculateMetric(isPercent, n.windows, d.windows).toNumber(), + linux: calculateMetric(isPercent, n.linux, d.linux).toNumber(), + macos: calculateMetric(isPercent, n.macos, d.macos).toNumber(), }; }; diff --git a/src/user/analytics/analyticsOverview/lib/overview.library.ts b/src/user/analytics/analyticsOverview/lib/overview.library.ts index 4a23f967..88723587 100644 --- a/src/user/analytics/analyticsOverview/lib/overview.library.ts +++ b/src/user/analytics/analyticsOverview/lib/overview.library.ts @@ -8,6 +8,7 @@ import { Tooltip, } from "user/analytics/analyticsOverview/types"; import { EngagementFragment } from "graphql/analytics-overview.generated"; +import BigNumber from "bignumber.js"; type MetricDataSet = { metric1DataSet: number[][]; @@ -176,54 +177,44 @@ const mapGroupingName = (grouping: string) => { }; export const mapMetric = (engagement: EngagementFragment): BaseMetric => { - const byType = (type: string, e: EngagementFragment) => - e.type === type ? e.count : 0; - return { - views: byType("view", engagement), - conversions: byType("conversion", engagement), - landings: byType("landed", engagement), - clicks: byType("click", engagement), - spend: engagement.cost, - upvotes: byType("upvote", engagement), - downvotes: byType("downvote", engagement), - dismissals: byType("dismiss", engagement), + views: BigNumber(engagement.view), + conversions: BigNumber(engagement.conversion), + landings: BigNumber(engagement.landed), + clicks: BigNumber(engagement.click), + spend: BigNumber(engagement.spend), + upvotes: BigNumber(engagement.upvote), + downvotes: BigNumber(engagement.downvote), + dismissals: BigNumber(engagement.dismiss), + clickthroughConversion: BigNumber(engagement.clickthroughConversion), + viewthroughConversion: BigNumber(engagement.viewthroughConversion), }; }; export const reduceMetric = (a: BaseMetric, b: BaseMetric) => { return { - views: a.views + b.views, - conversions: a.conversions + b.conversions, - landings: a.landings + b.landings, - clicks: a.clicks + b.clicks, - spend: a.spend + b.spend, - upvotes: a.upvotes + b.upvotes, - downvotes: a.downvotes + b.downvotes, - dismissals: a.dismissals + b.dismissals, + views: a.views.plus(b.views), + conversions: a.conversions.plus(b.conversions), + landings: a.landings.plus(b.landings), + clicks: a.clicks.plus(b.clicks), + spend: a.spend.plus(b.spend), + upvotes: a.upvotes.plus(b.upvotes), + downvotes: a.downvotes.plus(b.downvotes), + dismissals: a.dismissals.plus(b.dismissals), + clickthroughConversion: a.clickthroughConversion.plus( + b.clickthroughConversion, + ), + viewthroughConversion: a.viewthroughConversion.plus( + b.viewthroughConversion, + ), }; }; export const processStats = ( engagements: EngagementFragment[], -): StatsMetric => { +): StatsMetric | null => { if (engagements.length === 0) { - return { - clicks: 0, - convRate: 0, - conversions: 0, - cpa: 0, - ctr: 0, - dismissRate: 0, - dismissals: 0, - downvotes: 0, - landingRate: 0, - landings: 0, - spend: 0, - upvotes: 0, - views: 0, - visitRate: 0, - }; + return null; } const reduced = engagements.map(mapMetric).reduce(reduceMetric); @@ -241,17 +232,15 @@ export const processStats = ( export function calculateMetric( isPercent: boolean, - numerator: number, - denominator: number, + numerator: BigNumber | number, + denominator: BigNumber | number, ) { - let metric: number; + let metric = BigNumber(numerator).dividedBy(denominator); if (isPercent) { - metric = (numerator / denominator) * 100; - } else { - metric = numerator / denominator; + metric = metric.multipliedBy(100); } - return metric === Infinity ? 0 : metric; + return !metric.isFinite() ? BigNumber(0) : metric.dp(3); } export const processData = ( @@ -279,21 +268,24 @@ export const processData = ( const date = moment(key).valueOf(); const data = groupedData[key]; const processed = processStats(data); + if (!processed) { + continue; + } if (metric1) { - metric1DataSet.push([date, processed[metric1.key]]); + metric1DataSet.push([date, processed[metric1.key].toNumber()]); } if (metric2) { - metric2DataSet.push([date, processed[metric2.key]]); + metric2DataSet.push([date, processed[metric2.key].toNumber()]); } if (metric3) { - metric3DataSet.push([date, processed[metric3.key]]); + metric3DataSet.push([date, processed[metric3.key].toNumber()]); } if (metric4) { - metric4DataSet.push([date, processed[metric4.key]]); + metric4DataSet.push([date, processed[metric4.key].toNumber()]); } } diff --git a/src/user/analytics/analyticsOverview/reports/campaign/EngagementsOverview.tsx b/src/user/analytics/analyticsOverview/reports/campaign/EngagementsOverview.tsx index c778024a..94ee9b84 100644 --- a/src/user/analytics/analyticsOverview/reports/campaign/EngagementsOverview.tsx +++ b/src/user/analytics/analyticsOverview/reports/campaign/EngagementsOverview.tsx @@ -69,17 +69,17 @@ export function EngagementsOverview({ } if (!engagements || engagements.length === 0) { - return ( - - Reporting not available yet for {campaign.name}. - - ); + return ; } const processedData = processData(engagements, metrics, grouping); const processedStats = processStats(engagements); const options = prepareChart(metrics, processedData); + if (!processedStats) { + return ; + } + return ( ); } + +function ReportingNotReady(props: { campaignName: string }) { + return ( + + Reporting not available yet for {props.campaignName}. + + ); +} diff --git a/src/user/analytics/analyticsOverview/types/index.ts b/src/user/analytics/analyticsOverview/types/index.ts index 98f13ea6..ac2380d8 100644 --- a/src/user/analytics/analyticsOverview/types/index.ts +++ b/src/user/analytics/analyticsOverview/types/index.ts @@ -1,3 +1,5 @@ +import BigNumber from "bignumber.js"; + export type Metrics = { metric1?: { key: keyof StatsMetric; active: boolean }; metric2?: { key: keyof StatsMetric; active: boolean }; @@ -14,23 +16,25 @@ export type OS = { }; export type BaseMetric = { - views: number; - clicks: number; - conversions: number; - landings: number; - spend: number; - upvotes: number; - downvotes: number; - dismissals: number; + views: BigNumber; + clicks: BigNumber; + conversions: BigNumber; + landings: BigNumber; + spend: BigNumber; + upvotes: BigNumber; + downvotes: BigNumber; + dismissals: BigNumber; + clickthroughConversion: BigNumber; + viewthroughConversion: BigNumber; }; export type StatsMetric = BaseMetric & { - ctr: number; - convRate: number; - landingRate: number; - dismissRate: number; - cpa: number; - visitRate: number; + ctr: BigNumber; + convRate: BigNumber; + landingRate: BigNumber; + dismissRate: BigNumber; + cpa: BigNumber; + visitRate: BigNumber; }; export type Tooltip = { @@ -44,8 +48,6 @@ export type CreativeMetric = StatsMetric & { creativePayload: { title: string; body: string }; }; -export type EngagementChartType = "campaign" | "creative" | "creativeset"; - export interface Option { value: string; label: string; diff --git a/src/user/analytics/renderers/index.tsx b/src/user/analytics/renderers/index.tsx index 6c4ce40d..51f943fa 100644 --- a/src/user/analytics/renderers/index.tsx +++ b/src/user/analytics/renderers/index.tsx @@ -3,6 +3,7 @@ import { renderMonetaryAmount } from "components/Datagrid/renderers"; import { CampaignSummaryFragment } from "graphql/campaign.generated"; import { CampaignFormat } from "graphql/types"; import { StatsMetric } from "user/analytics/analyticsOverview/types"; +import { toLocaleString } from "util/bignumber"; export type EngagementOverview = { campaignId: string; @@ -61,7 +62,11 @@ export const renderStatsCell = ( return ; } - if (!val || val[type] <= 0) { + if (!val || !val[type]) { + return -; + } + + if (val[type].lte(0) || val[type].isNaN()) { return -; } @@ -71,11 +76,11 @@ export const renderStatsCell = ( case "dismissRate": case "landingRate": case "visitRate": - return {val[type].toLocaleString()}%; + return {toLocaleString(val[type])}%; case "spend": case "cpa": return renderMonetaryAmount(val.spend, currency ?? "USD"); default: - return {val[type].toLocaleString()}; + return {toLocaleString(val[type])}; } }; diff --git a/src/user/campaignList/CampaignList.tsx b/src/user/campaignList/CampaignList.tsx index a2e264a4..b994fd60 100644 --- a/src/user/campaignList/CampaignList.tsx +++ b/src/user/campaignList/CampaignList.tsx @@ -124,7 +124,9 @@ export function CampaignList({ advertiser }: Props) { { field: "view", headerName: "Impressions", - valueGetter: ({ row }) => engagementData?.get(row.id)?.["view"] ?? "N/A", + type: "number", + valueGetter: ({ row }) => + engagementData?.get(row.id)?.["view"]?.toString(), renderCell: ({ row }) => renderEngagementCell(loading, row, "view", engagementData), align: "right", @@ -135,7 +137,9 @@ export function CampaignList({ advertiser }: Props) { { field: "click", headerName: "Clicks", - valueGetter: ({ row }) => engagementData?.get(row.id)?.["click"] ?? "N/A", + type: "number", + valueGetter: ({ row }) => + engagementData?.get(row.id)?.["click"]?.toString(), renderCell: ({ row }) => renderEngagementCell(loading, row, "click", engagementData), align: "right", @@ -146,8 +150,9 @@ export function CampaignList({ advertiser }: Props) { { field: "landed", headerName: "Site visits", + type: "number", valueGetter: ({ row }) => - engagementData?.get(row.id)?.["landed"] ?? "N/A", + engagementData?.get(row.id)?.["landed"]?.toString(), renderCell: ({ row }) => renderEngagementCell(loading, row, "landed", engagementData), align: "right", @@ -158,8 +163,9 @@ export function CampaignList({ advertiser }: Props) { { field: "ctr", headerName: "CTR", + type: "number", valueGetter: ({ row }) => - getStatFromEngagement(row, "click", "view", engagementData), + getStatFromEngagement(row, "click", "view", engagementData)?.toString(), renderCell: ({ row }) => renderStatsCell( loading, diff --git a/src/user/views/user/AdDetailTable.tsx b/src/user/views/user/AdDetailTable.tsx index c64e6c1c..58ade9b3 100644 --- a/src/user/views/user/AdDetailTable.tsx +++ b/src/user/views/user/AdDetailTable.tsx @@ -26,7 +26,7 @@ export function AdDetailTable({ { field: "spend", headerName: "Spend", - valueGetter: ({ row }) => engagements.get(row.id)?.spend ?? "N/A", + valueGetter: ({ row }) => engagements.get(row.id)?.spend?.toNumber(), renderCell: ({ row }) => renderStatsCell( loading, @@ -36,52 +36,51 @@ export function AdDetailTable({ ), align: "right", headerAlign: "right", - minWidth: 100, - maxWidth: 250, + width: 100, }, { field: "view", + type: "number", headerName: "Impressions", - valueGetter: ({ row }) => engagements.get(row.id)?.views ?? "N/A", + valueGetter: ({ row }) => engagements.get(row.id)?.views?.toString(), renderCell: ({ row }) => renderStatsCell(loading, "views", engagements.get(row.id)), align: "right", headerAlign: "right", - minWidth: 100, - maxWidth: 250, + width: 155, }, { field: "click", + type: "number", headerName: "Clicks", - valueGetter: ({ row }) => engagements.get(row.id)?.clicks, + valueGetter: ({ row }) => engagements.get(row.id)?.clicks?.toString(), renderCell: ({ row }) => renderStatsCell(loading, "clicks", engagements.get(row.id)), align: "right", headerAlign: "right", - minWidth: 100, - maxWidth: 250, + width: 125, }, { field: "landed", + type: "number", headerName: "Site Visits", - valueGetter: ({ row }) => engagements.get(row.id)?.landings, + valueGetter: ({ row }) => engagements.get(row.id)?.landings?.toString(), renderCell: ({ row }) => renderStatsCell(loading, "landings", engagements.get(row.id)), align: "right", headerAlign: "right", - minWidth: 100, - maxWidth: 250, + width: 125, }, { field: "ctr", + type: "number", headerName: "CTR", - valueGetter: ({ row }) => engagements.get(row.id)?.ctr, + valueGetter: ({ row }) => engagements.get(row.id)?.ctr?.toString(), renderCell: ({ row }) => renderStatsCell(loading, "ctr", engagements.get(row.id)), align: "right", headerAlign: "right", - minWidth: 100, - maxWidth: 250, + width: 100, }, ); } @@ -98,7 +97,7 @@ export function AdDetailTable({ sx={{ borderStyle: "none" }} initialState={{ sorting: { - sortModel: [{ field: "createdAt", sort: "desc" }], + sortModel: [{ field: "name", sort: "desc" }], }, pagination: { paginationModel: { diff --git a/src/util/bignumber.ts b/src/util/bignumber.ts new file mode 100644 index 00000000..3fa7b8a1 --- /dev/null +++ b/src/util/bignumber.ts @@ -0,0 +1,10 @@ +import BigNumber from "bignumber.js"; + +export const toLocaleString = (b?: BigNumber | number | string) => { + if (!b) return "0"; + + return BigNumber(b).dp(2).toNumber().toLocaleString("en", { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }); +};