diff --git a/audit-resolve.json b/audit-resolve.json index 0967ef42..bd3cea1d 100644 --- a/audit-resolve.json +++ b/audit-resolve.json @@ -1 +1,11 @@ -{} +{ + "decisions": { + "1096303|vite-plugin-checker>lodash.pick": { + "decision": "ignore", + "madeAt": 1706290095658, + "expiresAt": 1708882081719 + } + }, + "rules": {}, + "version": 1 +} diff --git a/branding.svg b/branding.svg deleted file mode 100644 index 6299e205..00000000 --- a/branding.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/brave-ads-black.svg b/brave-ads-black.svg deleted file mode 100644 index a89dd54b..00000000 --- a/brave-ads-black.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/brave-logotype-full-color.png b/brave-logotype-full-color.png deleted file mode 100644 index 0b9efd3d..00000000 Binary files a/brave-logotype-full-color.png and /dev/null differ diff --git a/logo.svg b/logo.svg new file mode 100644 index 00000000..41995845 --- /dev/null +++ b/logo.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/AppBar/LandingPageAppBar.tsx b/src/components/AppBar/LandingPageAppBar.tsx index c4419235..e7b73060 100644 --- a/src/components/AppBar/LandingPageAppBar.tsx +++ b/src/components/AppBar/LandingPageAppBar.tsx @@ -7,7 +7,7 @@ import { Toolbar, Typography, } from "@mui/material"; -import ads from "../../../branding.svg"; +import ads from "../../../logo.svg"; import { Link as RouterLink, useRouteMatch } from "react-router-dom"; import { useIsAuthenticated } from "auth/hooks/queries/useIsAuthenticated"; import { useSignOut } from "auth/hooks/mutations/useSignOut"; diff --git a/src/components/Campaigns/CampaignDateRange.tsx b/src/components/Campaigns/CampaignDateRange.tsx index 76deb961..b5c27afa 100644 --- a/src/components/Campaigns/CampaignDateRange.tsx +++ b/src/components/Campaigns/CampaignDateRange.tsx @@ -23,9 +23,9 @@ export const CampaignDateRange = () => { value={parseISO(startMeta.value)} error={!!startMeta.error} helperText={startMeta.error} - onChange={(dt) => { - startHelper.setValue(formatISO(dt)); - startHelper.setTouched(true); + onChange={async (dt) => { + await startHelper.setValue(formatISO(dt)); + await startHelper.setTouched(true); }} disabled={!isDraft} /> @@ -38,9 +38,9 @@ export const CampaignDateRange = () => { value={parseISO(endMeta.value)} error={!!endMeta.error} helperText={endMeta.error} - onChange={(dt) => { - endHelper.setValue(formatISO(dt)); - endHelper.setTouched(true); + onChange={async (dt) => { + await endHelper.setValue(formatISO(dt)); + await endHelper.setTouched(true); }} /> 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/components/Datagrid/stringFilterOperators.tsx b/src/components/Datagrid/stringFilterOperators.tsx new file mode 100644 index 00000000..30b4fed9 --- /dev/null +++ b/src/components/Datagrid/stringFilterOperators.tsx @@ -0,0 +1,19 @@ +import { + getGridStringOperators, + GridFilterInputValue, + GridFilterItem, +} from "@mui/x-data-grid"; + +export function stringFilterOperators() { + return [ + { + label: "is not", + value: "not", + getApplyFilterFn: (field: GridFilterItem) => (params: any) => + params.value !== field.value, + InputComponentProps: { type: "string" }, + InputComponent: GridFilterInputValue, + }, + ...getGridStringOperators(), + ]; +} diff --git a/src/components/Navigation/Navbar.tsx b/src/components/Navigation/Navbar.tsx index 2ca6ed18..0e7c7fbd 100644 --- a/src/components/Navigation/Navbar.tsx +++ b/src/components/Navigation/Navbar.tsx @@ -1,7 +1,7 @@ import { AppBar, Button, Divider, Stack, Toolbar } from "@mui/material"; import { DraftMenu } from "components/Navigation/DraftMenu"; -import ads from "../../../branding.svg"; +import ads from "../../../logo.svg"; import { useAdvertiser } from "auth/hooks/queries/useAdvertiser"; import { useSignOut } from "auth/hooks/mutations/useSignOut"; import { NewCampaignButton } from "components/Navigation/NewCampaignButton"; diff --git a/src/components/Segment/SegmentPicker.tsx b/src/components/Segment/SegmentPicker.tsx index 248e4e53..d44b08de 100644 --- a/src/components/Segment/SegmentPicker.tsx +++ b/src/components/Segment/SegmentPicker.tsx @@ -41,7 +41,7 @@ export const SegmentPicker = ({ idx }: Props) => { {!targetMeta.value && ( @@ -71,10 +71,11 @@ export const SegmentPicker = ({ idx }: Props) => { {...params} label="Audiences" helperText={ - meta.error ?? - "Select the audience segments to target. Brave will decide if left untargeted." + meta.touched && !!meta.error + ? meta.error + : "Select the audience segments to target. Brave will decide if left untargeted." } - error={!!meta.error} + error={meta.touched && !!meta.error} /> )} isOptionEqualToValue={(option, value) => option.code === value.code} 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/ads/ShowAdsButton.tsx b/src/user/ads/ShowAdsButton.tsx index 00415e17..6a1faea8 100644 --- a/src/user/ads/ShowAdsButton.tsx +++ b/src/user/ads/ShowAdsButton.tsx @@ -13,12 +13,12 @@ export function ShowAdsButton() { underline="none" variant="subtitle1" sx={{ cursor: "pointer" }} - onClick={() => { + onClick={async () => { setIsShowingAds(true); - helper.setValue(false); + await helper.setValue(false); }} > - Use previously created Ads + Use existing Ads ); } 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 75e68f03..b994fd60 100644 --- a/src/user/campaignList/CampaignList.tsx +++ b/src/user/campaignList/CampaignList.tsx @@ -25,6 +25,7 @@ import { EditButton } from "user/campaignList/EditButton"; import { calculateMetric } from "user/analytics/analyticsOverview/lib/overview.library"; import { StatsMetric } from "user/analytics/analyticsOverview/types"; import { uiLabelsForCampaignFormat } from "util/campaign"; +import { stringFilterOperators } from "components/Datagrid/stringFilterOperators"; interface Props { advertiser?: AdvertiserCampaignsFragment | null; @@ -87,6 +88,7 @@ export function CampaignList({ advertiser }: Props) { align: "left", headerAlign: "left", width: 150, + filterOperators: stringFilterOperators(), }, { field: "state", @@ -122,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", @@ -133,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", @@ -144,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", @@ -156,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, @@ -256,6 +264,13 @@ export function CampaignList({ advertiser }: Props) { pageSize: 10, }, }, + filter: { + filterModel: { + items: [ + { field: "format", operator: "not", value: "New tab takeover" }, + ], + }, + }, }} /> ); diff --git a/src/user/views/adsManager/types/index.ts b/src/user/views/adsManager/types/index.ts index e69b53cf..c0e1b80e 100644 --- a/src/user/views/adsManager/types/index.ts +++ b/src/user/views/adsManager/types/index.ts @@ -101,8 +101,8 @@ export const initialCreative: Creative = { export const initialAdSet: AdSetForm = { name: "", - isNotTargeting: true, - segments: [{ code: "Svp7l-zGN", name: "untargeted" }], + isNotTargeting: false, + segments: [], conversions: [], oses: [], creatives: [], diff --git a/src/user/views/adsManager/views/advanced/components/adSet/fields/ConversionField.tsx b/src/user/views/adsManager/views/advanced/components/adSet/fields/ConversionField.tsx index f345c5a0..9f5bfb0f 100644 --- a/src/user/views/adsManager/views/advanced/components/adSet/fields/ConversionField.tsx +++ b/src/user/views/adsManager/views/advanced/components/adSet/fields/ConversionField.tsx @@ -1,8 +1,9 @@ -import { Link, Stack, Typography } from "@mui/material"; +import { Button, Link, Stack, Typography } from "@mui/material"; import { ConversionFields } from "components/Conversion/ConversionFields"; import { FieldArray, FieldArrayRenderProps, useField } from "formik"; import { Conversion, initialConversion } from "../../../../../types"; import { CardContainer } from "components/Card/CardContainer"; +import { Add } from "@mui/icons-material"; interface Props { index: number; @@ -11,27 +12,31 @@ interface Props { export function ConversionField({ index }: Props) { const [, meta] = useField(`adSets.${index}.conversions`); const conversions = meta.value ?? []; + const hasConversions = conversions.length > 0; return ( {(helper: FieldArrayRenderProps) => ( <> - + Define post-engagement analytics. - {conversions.length === 0 && ( - helper.push(initialConversion)} - sx={{ cursor: "pointer" }} + sx={{ + maxWidth: 300, + borderRadius: "16px", + }} + endIcon={} > - Add Conversion Tracking + - + Add Conversion tracking + )} - {conversions.length === 1 && ( + {hasConversions && ( ("format"); return ( - <> + + + Select the interest segments and platforms you would like to target. + {format.value !== CampaignFormat.NewsDisplayAd && ( - - - Select the audience you would like to advertise to by interests. - - - + )} - - - Select the devices and platforms you would like to advertise to. - - - - + + ); } diff --git a/src/user/views/adsManager/views/advanced/components/campaign/BudgetSettings.tsx b/src/user/views/adsManager/views/advanced/components/campaign/BudgetSettings.tsx deleted file mode 100644 index b4175ea0..00000000 --- a/src/user/views/adsManager/views/advanced/components/campaign/BudgetSettings.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { BudgetField } from "user/views/adsManager/views/advanced/components/campaign/fields/BudgetField"; -import { PaymentMethodField } from "user/views/adsManager/views/advanced/components/campaign/fields/PaymentMethodField"; - -export function BudgetSettings() { - return ( - <> - - - - - ); -} diff --git a/src/user/views/adsManager/views/advanced/components/campaign/CampaignSettings.tsx b/src/user/views/adsManager/views/advanced/components/campaign/CampaignSettings.tsx index 4c233333..dcfacce0 100644 --- a/src/user/views/adsManager/views/advanced/components/campaign/CampaignSettings.tsx +++ b/src/user/views/adsManager/views/advanced/components/campaign/CampaignSettings.tsx @@ -5,24 +5,39 @@ import { LocationField } from "user/views/adsManager/views/advanced/components/c import { Typography } from "@mui/material"; import { FormatField } from "user/views/adsManager/views/advanced/components/campaign/fields/FormatField"; import { AdvertiserPrice } from "user/hooks/useAdvertiserWithPrices"; +import { BudgetField } from "user/views/adsManager/views/advanced/components/campaign/fields/BudgetField"; +import { BillingModelSelect } from "user/views/adsManager/views/advanced/components/campaign/components/BillingModelSelect"; +import { CustomPriceSelect } from "user/views/adsManager/views/advanced/components/campaign/components/CustomPriceSelect"; +import { useAdvertiser } from "auth/hooks/queries/useAdvertiser"; export function CampaignSettings(props: { prices: AdvertiserPrice[] }) { const { isDraft } = useIsEdit(); + const { advertiser } = useAdvertiser(); return ( <> - Define when you want your campaign to run. + Define how you want your campaign to run. + + + + {!advertiser.selfServiceSetPrice && ( + + )} + + {advertiser.selfServiceSetPrice && } + + {isDraft && } ); diff --git a/src/user/views/adsManager/views/advanced/components/campaign/components/BillingModelSelect.tsx b/src/user/views/adsManager/views/advanced/components/campaign/components/BillingModelSelect.tsx index 731a46c0..171a1b04 100644 --- a/src/user/views/adsManager/views/advanced/components/campaign/components/BillingModelSelect.tsx +++ b/src/user/views/adsManager/views/advanced/components/campaign/components/BillingModelSelect.tsx @@ -17,7 +17,8 @@ export function BillingModelSelect(props: { prices: AdvertiserPrice[] }) { return ( - {uiLabelsForCampaignFormat(format.value)} billing configuration + {uiLabelsForCampaignFormat(format.value)} pricing configuration + option(s) {props.prices diff --git a/src/user/views/adsManager/views/advanced/components/campaign/fields/BudgetField.tsx b/src/user/views/adsManager/views/advanced/components/campaign/fields/BudgetField.tsx index eab3b405..4cdcd40b 100644 --- a/src/user/views/adsManager/views/advanced/components/campaign/fields/BudgetField.tsx +++ b/src/user/views/adsManager/views/advanced/components/campaign/fields/BudgetField.tsx @@ -1,20 +1,17 @@ -import { InputAdornment, Stack, Typography } from "@mui/material"; -import { FormikTextField, useIsEdit } from "form/FormikHelpers"; -import { useEffect, useState } from "react"; -import { useField, useFormikContext } from "formik"; -import { CampaignForm } from "../../../../../types"; -import { differenceInHours } from "date-fns"; -import { MIN_PER_CAMPAIGN, MIN_PER_DAY } from "validation/CampaignSchema"; -import { CardContainer } from "components/Card/CardContainer"; -import { useAdvertiserWithPrices } from "user/hooks/useAdvertiserWithPrices"; -import { BillingModelSelect } from "../components/BillingModelSelect"; -import { CustomPriceSelect } from "../components/CustomPriceSelect"; +import {InputAdornment} from "@mui/material"; +import {FormikTextField, useIsEdit} from "form/FormikHelpers"; +import {useEffect, useState} from "react"; +import {useField, useFormikContext} from "formik"; +import {CampaignForm} from "../../../../../types"; +import {differenceInHours} from "date-fns"; +import {MIN_PER_CAMPAIGN, MIN_PER_DAY} from "validation/CampaignSchema"; +import {useAdvertiserWithPrices} from "user/hooks/useAdvertiserWithPrices"; export function BudgetField() { const [, , dailyBudget] = useField("dailyBudget"); - const { isDraft } = useIsEdit(); - const { data } = useAdvertiserWithPrices(); - const { values, errors } = useFormikContext(); + const {isDraft} = useIsEdit(); + const {data} = useAdvertiserWithPrices(); + const {values, errors} = useFormikContext(); const [minBudget, setMinBudget] = useState(MIN_PER_CAMPAIGN); const campaignRuntime = Math.floor( differenceInHours(new Date(values.endAt), new Date(values.startAt)) / 24, @@ -38,37 +35,24 @@ export function BudgetField() { }, [campaignRuntime, values.budget, minBudget]); return ( - - - Set a limit on how much your campaign will spend. - - - $, - endAdornment: ( - {values.currency} - ), - }} - helperText={ - errors.budget || errors.dailyBudget - ? `${errors.dailyBudget}. Minimum $${minBudget}.` - : undefined - } - error={!!errors.budget || !!errors.dailyBudget} - disabled={!isDraft && !data.selfServiceSetPrice} - /> - - {!data.selfServiceSetPrice && ( - - )} - - {data.selfServiceSetPrice && } - - + $, + endAdornment: ( + {values.currency} + ), + }} + helperText={ + errors.budget || errors.dailyBudget + ? `${errors.dailyBudget}. Minimum $${minBudget}.` + : undefined + } + error={!!errors.budget || !!errors.dailyBudget} + disabled={!isDraft && !data.selfServiceSetPrice} + /> ); } diff --git a/src/user/views/adsManager/views/advanced/components/form/components/BaseForm.tsx b/src/user/views/adsManager/views/advanced/components/form/components/BaseForm.tsx index 2bdd6335..d7b3ac81 100644 --- a/src/user/views/adsManager/views/advanced/components/form/components/BaseForm.tsx +++ b/src/user/views/adsManager/views/advanced/components/form/components/BaseForm.tsx @@ -6,7 +6,6 @@ import { PaymentButton } from "user/views/adsManager/views/advanced/components/f import { AdSetFields } from "user/views/adsManager/views/advanced/components/adSet/AdSetFields"; import { NewAdSet } from "user/views/adsManager/views/advanced/components/adSet/NewAdSet"; import { Route, Switch, useRouteMatch } from "react-router-dom"; -import { BudgetSettings } from "user/views/adsManager/views/advanced/components/campaign/BudgetSettings"; import { FormContext } from "state/context"; import { useState } from "react"; import { AdvertiserPrice } from "user/hooks/useAdvertiserWithPrices"; @@ -26,11 +25,6 @@ export function BaseForm({ hasPaymentIntent, prices }: Props) { path: `${url}/settings`, component: , }, - { - label: "Budget", - path: `${url}/budget`, - component: , - }, { label: "Ad Sets", path: `${url}/adSets`, diff --git a/src/user/views/adsManager/views/advanced/components/review/Review.tsx b/src/user/views/adsManager/views/advanced/components/review/Review.tsx index 80bc4b13..6e3de461 100644 --- a/src/user/views/adsManager/views/advanced/components/review/Review.tsx +++ b/src/user/views/adsManager/views/advanced/components/review/Review.tsx @@ -4,6 +4,7 @@ import { Box } from "@mui/material"; import { useEffect } from "react"; import { CampaignReview } from "./components/CampaignReview"; import { AdSetReview } from "./components/AdSetReview"; +import { PaymentMethodField } from "user/views/adsManager/views/advanced/components/campaign/fields/PaymentMethodField"; export function Review() { const { values, errors, setTouched } = useFormikContext(); @@ -28,6 +29,8 @@ export function Review() { errors={errors.adSets?.[adSetIdx]} /> ))} + + ); } 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, + }); +}; diff --git a/src/validation/CampaignSchema.tsx b/src/validation/CampaignSchema.tsx index e42eed7e..bd0962f0 100644 --- a/src/validation/CampaignSchema.tsx +++ b/src/validation/CampaignSchema.tsx @@ -20,7 +20,7 @@ import { Billing } from "user/views/adsManager/types"; import { uiLabelsForCampaignFormat } from "util/campaign"; export const MIN_PER_DAY = 33; -export const MIN_PER_CAMPAIGN = 100; +export const MIN_PER_CAMPAIGN = 500; export const CampaignSchema = (prices: AdvertiserPrice[]) => object().shape({