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({