diff --git a/src/components/Drawer/MiniSideBar.tsx b/src/components/Drawer/MiniSideBar.tsx
index ac761567..80dadc2a 100644
--- a/src/components/Drawer/MiniSideBar.tsx
+++ b/src/components/Drawer/MiniSideBar.tsx
@@ -43,23 +43,21 @@ export default function MiniSideBar({ children }: PropsWithChildren) {
/>
),
},
- // Possible future enhancements, not visible to user but help keep spacing
{
- label: "Creatives",
- href: "/user/main/creatives",
+ label: "Assets",
+ href: "/user/main/assets",
icon: (
-
),
- disabled: true,
},
{
- label: "Assets",
- href: "/user/main/assets",
+ label: "Creatives",
+ href: "/user/main/creatives",
icon: (
-
diff --git a/src/graphql/advertiser.generated.tsx b/src/graphql/advertiser.generated.tsx
index 8e9129df..86756ce7 100644
--- a/src/graphql/advertiser.generated.tsx
+++ b/src/graphql/advertiser.generated.tsx
@@ -124,6 +124,44 @@ export type AdvertiserCampaignsQuery = {
} | null;
};
+export type AdvertiserImageFragment = {
+ name: string;
+ imageUrl: string;
+ format: Types.CampaignFormat;
+ id: string;
+ createdAt: any;
+};
+
+export type AdvertiserImagesQueryVariables = Types.Exact<{
+ id: Types.Scalars["String"];
+}>;
+
+export type AdvertiserImagesQuery = {
+ advertiser?: {
+ images: Array<{
+ name: string;
+ imageUrl: string;
+ format: Types.CampaignFormat;
+ id: string;
+ createdAt: any;
+ }>;
+ } | null;
+};
+
+export type UploadAdvertiserImageMutationVariables = Types.Exact<{
+ input: Types.CreateAdvertiserImageInput;
+}>;
+
+export type UploadAdvertiserImageMutation = {
+ createAdvertiserImage: {
+ name: string;
+ imageUrl: string;
+ format: Types.CampaignFormat;
+ id: string;
+ createdAt: any;
+ };
+};
+
export const AdvertiserSummaryFragmentDoc = gql`
fragment AdvertiserSummary on Advertiser {
id
@@ -168,6 +206,15 @@ export const AdvertiserCampaignsFragmentDoc = gql`
}
${CampaignSummaryFragmentDoc}
`;
+export const AdvertiserImageFragmentDoc = gql`
+ fragment AdvertiserImage on AdvertiserImage {
+ name
+ imageUrl
+ format
+ id
+ createdAt
+ }
+`;
export const AdvertiserDocument = gql`
query advertiser($id: String!) {
advertiser(id: $id) {
@@ -344,3 +391,120 @@ export function refetchAdvertiserCampaignsQuery(
) {
return { query: AdvertiserCampaignsDocument, variables: variables };
}
+export const AdvertiserImagesDocument = gql`
+ query advertiserImages($id: String!) {
+ advertiser(id: $id) {
+ images {
+ ...AdvertiserImage
+ }
+ }
+ }
+ ${AdvertiserImageFragmentDoc}
+`;
+
+/**
+ * __useAdvertiserImagesQuery__
+ *
+ * To run a query within a React component, call `useAdvertiserImagesQuery` and pass it any options that fit your needs.
+ * When your component renders, `useAdvertiserImagesQuery` returns an object from Apollo Client that contains loading, error, and data properties
+ * you can use to render your UI.
+ *
+ * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
+ *
+ * @example
+ * const { data, loading, error } = useAdvertiserImagesQuery({
+ * variables: {
+ * id: // value for 'id'
+ * },
+ * });
+ */
+export function useAdvertiserImagesQuery(
+ baseOptions: Apollo.QueryHookOptions<
+ AdvertiserImagesQuery,
+ AdvertiserImagesQueryVariables
+ >,
+) {
+ const options = { ...defaultOptions, ...baseOptions };
+ return Apollo.useQuery(
+ AdvertiserImagesDocument,
+ options,
+ );
+}
+export function useAdvertiserImagesLazyQuery(
+ baseOptions?: Apollo.LazyQueryHookOptions<
+ AdvertiserImagesQuery,
+ AdvertiserImagesQueryVariables
+ >,
+) {
+ const options = { ...defaultOptions, ...baseOptions };
+ return Apollo.useLazyQuery<
+ AdvertiserImagesQuery,
+ AdvertiserImagesQueryVariables
+ >(AdvertiserImagesDocument, options);
+}
+export type AdvertiserImagesQueryHookResult = ReturnType<
+ typeof useAdvertiserImagesQuery
+>;
+export type AdvertiserImagesLazyQueryHookResult = ReturnType<
+ typeof useAdvertiserImagesLazyQuery
+>;
+export type AdvertiserImagesQueryResult = Apollo.QueryResult<
+ AdvertiserImagesQuery,
+ AdvertiserImagesQueryVariables
+>;
+export function refetchAdvertiserImagesQuery(
+ variables: AdvertiserImagesQueryVariables,
+) {
+ return { query: AdvertiserImagesDocument, variables: variables };
+}
+export const UploadAdvertiserImageDocument = gql`
+ mutation uploadAdvertiserImage($input: CreateAdvertiserImageInput!) {
+ createAdvertiserImage(createImageInput: $input) {
+ ...AdvertiserImage
+ }
+ }
+ ${AdvertiserImageFragmentDoc}
+`;
+export type UploadAdvertiserImageMutationFn = Apollo.MutationFunction<
+ UploadAdvertiserImageMutation,
+ UploadAdvertiserImageMutationVariables
+>;
+
+/**
+ * __useUploadAdvertiserImageMutation__
+ *
+ * To run a mutation, you first call `useUploadAdvertiserImageMutation` within a React component and pass it any options that fit your needs.
+ * When your component renders, `useUploadAdvertiserImageMutation` returns a tuple that includes:
+ * - A mutate function that you can call at any time to execute the mutation
+ * - An object with fields that represent the current status of the mutation's execution
+ *
+ * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
+ *
+ * @example
+ * const [uploadAdvertiserImageMutation, { data, loading, error }] = useUploadAdvertiserImageMutation({
+ * variables: {
+ * input: // value for 'input'
+ * },
+ * });
+ */
+export function useUploadAdvertiserImageMutation(
+ baseOptions?: Apollo.MutationHookOptions<
+ UploadAdvertiserImageMutation,
+ UploadAdvertiserImageMutationVariables
+ >,
+) {
+ const options = { ...defaultOptions, ...baseOptions };
+ return Apollo.useMutation<
+ UploadAdvertiserImageMutation,
+ UploadAdvertiserImageMutationVariables
+ >(UploadAdvertiserImageDocument, options);
+}
+export type UploadAdvertiserImageMutationHookResult = ReturnType<
+ typeof useUploadAdvertiserImageMutation
+>;
+export type UploadAdvertiserImageMutationResult =
+ Apollo.MutationResult;
+export type UploadAdvertiserImageMutationOptions = Apollo.BaseMutationOptions<
+ UploadAdvertiserImageMutation,
+ UploadAdvertiserImageMutationVariables
+>;
diff --git a/src/graphql/advertiser.graphql b/src/graphql/advertiser.graphql
index e14d5db5..131a8440 100644
--- a/src/graphql/advertiser.graphql
+++ b/src/graphql/advertiser.graphql
@@ -56,3 +56,25 @@ query advertiserCampaigns($id: String!, $filter: AdvertiserCampaignFilter) {
...AdvertiserCampaigns
}
}
+
+fragment AdvertiserImage on AdvertiserImage {
+ name
+ imageUrl
+ format
+ id
+ createdAt
+}
+
+query advertiserImages($id: String!) {
+ advertiser(id: $id) {
+ images {
+ ...AdvertiserImage
+ }
+ }
+}
+
+mutation uploadAdvertiserImage($input: CreateAdvertiserImageInput!) {
+ createAdvertiserImage(createImageInput: $input) {
+ ...AdvertiserImage
+ }
+}
diff --git a/src/graphql/types.ts b/src/graphql/types.ts
index 1c2e0c05..3e41ef7c 100644
--- a/src/graphql/types.ts
+++ b/src/graphql/types.ts
@@ -187,6 +187,7 @@ export type CreateCampaignInput = {
name: Scalars["String"];
pacingStrategy?: InputMaybe;
paymentType?: InputMaybe;
+ priority?: InputMaybe;
source: Scalars["String"];
startAt: Scalars["DateTime"];
state: Scalars["String"];
diff --git a/src/user/User.tsx b/src/user/User.tsx
index 8896321d..6ff7479c 100644
--- a/src/user/User.tsx
+++ b/src/user/User.tsx
@@ -93,6 +93,11 @@ export function User() {
unauthedComponent={AdvertiserAgreed}
/>
+
+
{/* default */}
diff --git a/src/user/hooks/useUploadFile.ts b/src/user/hooks/useUploadFile.ts
new file mode 100644
index 00000000..2e4de99f
--- /dev/null
+++ b/src/user/hooks/useUploadFile.ts
@@ -0,0 +1,127 @@
+import { buildAdServerEndpoint, getEnvConfig } from "util/environment";
+import { useCallback, useState } from "react";
+import _ from "lodash";
+import { useUploadAdvertiserImageMutation } from "graphql/advertiser.generated";
+import { useAdvertiser } from "auth/hooks/queries/useAdvertiser";
+import { CampaignFormat } from "graphql/types";
+import { UploadConfig } from "user/views/advertiser/UploadImage";
+
+interface PutUploadResponse {
+ // the pre-signed url to which the file should be uploaded to
+ uploadUrl: string;
+ // the path on the cdn that this url will eventually have
+ destinationPath: string;
+}
+
+export const useUploadFile = () => {
+ const { advertiser } = useAdvertiser();
+ const [error, setError] = useState();
+ const [step, setStep] = useState(0);
+ const [state, setState] = useState();
+ const [loading, setLoading] = useState(false);
+ const [mutate] = useUploadAdvertiserImageMutation({
+ onError(e) {
+ setError(e.message);
+ setLoading(false);
+ },
+ onCompleted(data) {
+ setStep(2);
+ setState(`File upload complete for ${data.createAdvertiserImage.name}!`);
+ setLoading(false);
+ },
+ });
+
+ const uploadFile = useCallback(async (file: File, format: CampaignFormat) => {
+ setError(undefined);
+ setLoading(true);
+ setState("Preparing file for upload...");
+
+ let upload: PutUploadResponse;
+ try {
+ const extension = _.last(file.name.split(".")) ?? "";
+ upload = await prepareForUpload(extension);
+ } catch (e: any) {
+ setError(e.message);
+ return;
+ }
+
+ try {
+ setState("Uploading file...");
+ await putFile(file, upload);
+ setStep(1);
+ } catch (e: any) {
+ setError(e.message);
+ return;
+ }
+
+ await mutate({
+ variables: {
+ input: {
+ format: format,
+ advertiserId: advertiser.id,
+ name: file.name,
+ imageUrl: `https://${configForFormat(format).targetHost}${
+ upload.destinationPath
+ }`,
+ },
+ },
+ });
+ }, []);
+
+ return [{ upload: uploadFile }, { state, step, loading, error }];
+};
+
+async function prepareForUpload(extension: string): Promise {
+ const resp = await fetch(
+ buildAdServerEndpoint(`/internal/image-upload/${extension}`),
+ {
+ method: "GET",
+ mode: "cors",
+ credentials: "include",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ },
+ );
+ if (!resp.ok) {
+ throw new Error(`Unable to upload image`);
+ }
+ const result = (await resp.json()) as PutUploadResponse;
+ if (!result.destinationPath || !result.uploadUrl) {
+ throw new Error(`Unable to upload image`);
+ }
+
+ return result;
+}
+
+async function putFile(file: File, uploadTarget: PutUploadResponse) {
+ try {
+ const resp = await fetch(uploadTarget.uploadUrl, {
+ method: "PUT",
+ mode: "cors",
+ body: file,
+ });
+
+ if (!resp.ok) {
+ await resp.text();
+ throw new Error(`Failed to upload image`);
+ }
+ } catch (e: any) {
+ if (e.message === "Failed to fetch") {
+ throw new Error(`Failed to upload image`);
+ }
+ throw e;
+ }
+}
+
+const configForFormat = (format: CampaignFormat): UploadConfig => {
+ if (format === CampaignFormat.NewsDisplayAd) {
+ return {
+ targetHost: () => getEnvConfig().pcdnHost,
+ requiresPublishStep: false,
+ endpoint: "internal/image-upload",
+ };
+ }
+
+ throw new Error("Invalid format");
+};
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 246d1558..20876549 100644
--- a/src/user/views/adsManager/views/advanced/components/campaign/CampaignSettings.tsx
+++ b/src/user/views/adsManager/views/advanced/components/campaign/CampaignSettings.tsx
@@ -20,7 +20,7 @@ export function CampaignSettings() {
- {isDraft && }
+
{isDraft && }
>
diff --git a/src/user/views/adsManager/views/advanced/components/campaign/fields/FormatField.tsx b/src/user/views/adsManager/views/advanced/components/campaign/fields/FormatField.tsx
index 35652ec2..e3e1419c 100644
--- a/src/user/views/adsManager/views/advanced/components/campaign/fields/FormatField.tsx
+++ b/src/user/views/adsManager/views/advanced/components/campaign/fields/FormatField.tsx
@@ -10,13 +10,14 @@ import { useField } from "formik";
import { CampaignFormat } from "graphql/types";
import _ from "lodash";
import HelpIcon from "@mui/icons-material/Help";
+import { useIsEdit } from "form/FormikHelpers";
export function FormatField() {
return (
- Choose what tye of campaign you would like to run
+ Choose a format for the campaign you would like to run
{
+ const { isEdit } = useIsEdit();
const [, meta, helper] = useField("format");
return (
helper.setValue(props.format)}
sx={{
diff --git a/src/user/views/advertiser/AdvertiserAssets.tsx b/src/user/views/advertiser/AdvertiserAssets.tsx
new file mode 100644
index 00000000..2c164a66
--- /dev/null
+++ b/src/user/views/advertiser/AdvertiserAssets.tsx
@@ -0,0 +1 @@
+export function AdvertiserAssets() {}
diff --git a/src/user/views/advertiser/UploadImage.tsx b/src/user/views/advertiser/UploadImage.tsx
new file mode 100644
index 00000000..870ed97d
--- /dev/null
+++ b/src/user/views/advertiser/UploadImage.tsx
@@ -0,0 +1,99 @@
+import { useState } from "react";
+import { useUploadFile } from "user/hooks/useUploadFile";
+import {
+ Alert,
+ Box,
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogContentText,
+ DialogTitle,
+ LinearProgress,
+ Step,
+ StepLabel,
+ Stepper,
+} from "@mui/material";
+import { CampaignFormat } from "graphql/types";
+
+export interface UploadConfig {
+ targetHost: () => string;
+ requiresPublishStep: boolean;
+ endpoint: string;
+}
+
+export function UploadImage() {
+ const [open, setOpen] = useState(false);
+ const [file, setFile] = useState();
+ const [{ upload }, { step, error, loading, state }] = useUploadFile();
+
+ return (
+
+
+
+
+ );
+}
diff --git a/src/util/environment.ts b/src/util/environment.ts
index edd6c2d1..7e19b15c 100644
--- a/src/util/environment.ts
+++ b/src/util/environment.ts
@@ -1,3 +1,39 @@
+export enum Environment {
+ LOCAL = "local",
+ STAGE = "stage",
+ PRODUCTION = "production",
+}
+
+export function getEnvironment(): Environment {
+ const host = window.location.hostname;
+
+ if (host.endsWith(".brave.com")) {
+ return Environment.PRODUCTION;
+ }
+
+ if (host.endsWith(".bravesoftware.com")) {
+ return Environment.STAGE;
+ }
+
+ return Environment.LOCAL;
+}
+
+interface EnvConfig {
+ pcdnHost: string;
+}
+
+export function getEnvConfig(): EnvConfig {
+ if (getEnvironment() === Environment.PRODUCTION) {
+ return {
+ pcdnHost: "pcdn.brave.com",
+ };
+ }
+
+ return {
+ pcdnHost: "pcdn.bravesoftware.com",
+ };
+}
+
export function buildAdServerEndpoint(suffix: string): string {
return `${import.meta.env.REACT_APP_SERVER_ADDRESS}${suffix}`;
}