From 00caefafd953651ca20cea99211242b4105b116e Mon Sep 17 00:00:00 2001 From: Ian Krieger Date: Thu, 31 Aug 2023 11:58:05 -0400 Subject: [PATCH] feat: add gallery, fix preview --- src/components/Assets/AdvertiserAssets.tsx | 93 ++++++++++--------- src/components/Assets/ImageAutocomplete.tsx | 63 +++++++------ src/components/Assets/ImagePreview.tsx | 21 ++++- src/components/Assets/UploadImage.tsx | 53 +++++++++-- src/components/Assets/hooks/useUploadFile.ts | 16 +++- src/components/Card/CardContainer.tsx | 5 +- ...ificationSelect.tsx => CreativeSelect.tsx} | 38 +++++++- .../Creatives/CreativeSpecificPreview.tsx | 11 +++ src/components/Creatives/NewsPreview.tsx | 16 +--- src/user/ads/AdsExistingAd.tsx | 4 +- src/user/ads/InlineContentAd.tsx | 2 +- src/user/ads/NewAd.tsx | 22 ++++- .../components/adSet/fields/AdSetAds.tsx | 10 +- src/user/views/user/CampaignView.tsx | 6 +- 14 files changed, 245 insertions(+), 115 deletions(-) rename src/components/Creatives/{NotificationSelect.tsx => CreativeSelect.tsx} (78%) diff --git a/src/components/Assets/AdvertiserAssets.tsx b/src/components/Assets/AdvertiserAssets.tsx index 60a37726..3a3ceb2f 100644 --- a/src/components/Assets/AdvertiserAssets.tsx +++ b/src/components/Assets/AdvertiserAssets.tsx @@ -3,17 +3,14 @@ import { useAdvertiserImagesQuery, } from "graphql/advertiser.generated"; import { useAdvertiser } from "auth/hooks/queries/useAdvertiser"; -import { - ColumnDescriptor, - EnhancedTable, - StandardRenderers, -} from "components/EnhancedTable"; import { ErrorDetail } from "components/Error/ErrorDetail"; import { CardContainer } from "components/Card/CardContainer"; -import { Box, Skeleton } from "@mui/material"; +import { Grid, LinearProgress, Typography } from "@mui/material"; import MiniSideBar from "components/Drawer/MiniSideBar"; import { UploadImage } from "components/Assets/UploadImage"; import { ImagePreview } from "components/Assets/ImagePreview"; +import { CampaignFormat } from "graphql/types"; +import moment from "moment/moment"; export function AdvertiserAssets() { const { advertiser } = useAdvertiser(); @@ -21,45 +18,57 @@ export function AdvertiserAssets() { variables: { id: advertiser.id }, }); - const options: ColumnDescriptor[] = [ - { - title: "Created", - value: (c) => c.createdAt, - renderer: StandardRenderers.date, - }, - { - title: "Name", - value: (c) => c.name, - }, - { - title: "Image", - value: (c) => c.imageUrl, - extendedRenderer: (r) => , - }, - ]; + if (loading) { + return ( + + + + ); + } + const images = (data?.advertiser?.images ?? []).sort( + (a, b) => a.createdAt.getTime() - b.createdAt.getTime(), + ); return ( - - {loading && ( - - - - )} - {error && ( - - )} - {!loading && !error && ( - - )} - - + {error && ( + + )} + {!loading && !error && ( + + {images.map((i, idx) => ( + + + + ))} + + + + + )} ); } + +const GalleryItem = (props: { image: AdvertiserImageFragment }) => { + const { name, imageUrl, createdAt, format } = props.image; + const height = format === CampaignFormat.NewsDisplayAd ? 400 : undefined; + const width = format === CampaignFormat.NewsDisplayAd ? 500 : undefined; + + return ( + + + + created {moment(createdAt).fromNow()} + + + ); +}; diff --git a/src/components/Assets/ImageAutocomplete.tsx b/src/components/Assets/ImageAutocomplete.tsx index 0bf95734..ed703e27 100644 --- a/src/components/Assets/ImageAutocomplete.tsx +++ b/src/components/Assets/ImageAutocomplete.tsx @@ -3,12 +3,14 @@ import { useAdvertiser } from "auth/hooks/queries/useAdvertiser"; import { Autocomplete, createFilterOptions, TextField } from "@mui/material"; import { useState } from "react"; import { useField } from "formik"; +import { UploadImage } from "components/Assets/UploadImage"; type ImageOption = { label: string; image?: string }; const filter = createFilterOptions(); export function ImageAutocomplete() { + const [createImage, setCreateImage] = useState(false); const [, meta, imageUrl] = useField( `newCreative.payloadInlineContent.imageUrl`, ); @@ -30,31 +32,40 @@ export function ImageAutocomplete() { }); return ( - ( - - )} - value={{ - label: options?.find((o) => o.image === meta.value)?.label ?? "", - image: meta.value, - }} - getOptionLabel={(o) => o.label} - filterOptions={(options, params) => { - const filtered = filter(options, params); - return [...filtered, { image: undefined, label: `Upload new image` }]; - }} - onChange={(e, nv) => { - imageUrl.setValue(nv ? nv.image : undefined); - }} - /> + <> + ( + + )} + value={{ + label: options?.find((o) => o.image === meta.value)?.label ?? "", + image: meta.value, + }} + getOptionLabel={(o) => o.label} + filterOptions={(options, params) => { + const filtered = filter(options, params); + return [...filtered, { image: undefined, label: `Upload new image` }]; + }} + onChange={(e, nv) => { + imageUrl.setValue(nv ? nv.image : undefined); + setCreateImage(nv != null && nv.image === undefined); + }} + /> + + setCreateImage(false)} + onComplete={(url) => imageUrl.setValue(url)} + /> + ); } diff --git a/src/components/Assets/ImagePreview.tsx b/src/components/Assets/ImagePreview.tsx index af6f71eb..1b4641a0 100644 --- a/src/components/Assets/ImagePreview.tsx +++ b/src/components/Assets/ImagePreview.tsx @@ -6,13 +6,19 @@ interface Props { url: string; height?: number; width?: number; + selected?: boolean; } -export const ImagePreview = ({ url, height = 300, width = 360 }: Props) => { +export const ImagePreview = ({ + url, + height = 300, + width = 360, + selected, +}: Props) => { const { data, loading, error } = useGetImagePreviewUrl({ url }); if (!data || loading) { - return ; + return ; } if (error) { @@ -28,7 +34,16 @@ export const ImagePreview = ({ url, height = 300, width = 360 }: Props) => { }} > {url?.endsWith(".pad") ? ( - preview + preview ) : ( string; @@ -23,14 +25,44 @@ export interface UploadConfig { endpoint: string; } -export function UploadImage() { +interface Props { + useInlineCreation?: boolean; + onComplete?: (url: string) => void; + onClose?: () => void; +} + +export function UploadImage({ useInlineCreation, onClose, onComplete }: Props) { const [open, setOpen] = useState(false); const [file, setFile] = useState(); - const [{ upload, reset }, { step, error, loading, state }] = useUploadFile(); + const [{ upload, reset }, { step, error, loading, state }] = useUploadFile({ + onComplete(data) { + if (useInlineCreation && onComplete) { + onComplete(data); + } + }, + }); + + useEffect(() => { + if (useInlineCreation) { + setOpen(true); + } + }, [useInlineCreation]); return ( - - + <> + {useInlineCreation === undefined && ( + + setOpen(true)} + width={200} + height={423} + > + + + + )} setOpen(false)}> Upload Image @@ -69,7 +101,11 @@ export function UploadImage() { )} {step === 0 && !!file && ( - setFile(undefined)} label={file.name} /> + setFile(undefined)} + label={file.name} + sx={{ marginBottom: 1 }} + /> )} {!error && state && ( @@ -85,6 +121,7 @@ export function UploadImage() { setOpen(false); setFile(undefined); reset!(); + if (onClose) onClose(); }} variant="outlined" > @@ -92,7 +129,7 @@ export function UploadImage() { {step !== 2 && ( - + ); } diff --git a/src/components/Assets/hooks/useUploadFile.ts b/src/components/Assets/hooks/useUploadFile.ts index fbb4afc0..e942607f 100644 --- a/src/components/Assets/hooks/useUploadFile.ts +++ b/src/components/Assets/hooks/useUploadFile.ts @@ -16,8 +16,13 @@ interface PutUploadResponse { destinationPath: string; } -export const useUploadFile = () => { +interface Props { + onComplete?: (data: string) => void; +} + +export const useUploadFile = ({ onComplete }: Props = {}) => { const { advertiser } = useAdvertiser(); + const [url, setUrl] = useState(); const [error, setError] = useState(); const [step, setStep] = useState(0); const [state, setState] = useState(); @@ -35,6 +40,7 @@ export const useUploadFile = () => { setStep(2); setState(`File upload complete for "${data.createAdvertiserImage.name}"`); setLoading(false); + if (onComplete && url) onComplete(url); }, }); @@ -64,15 +70,17 @@ export const useUploadFile = () => { return; } + const imageUrl = `https://${configForFormat(format).targetHost()}${ + upload.destinationPath + }`; + setUrl(imageUrl); await mutate({ variables: { input: { format: format, advertiserId: advertiser.id, name: file.name, - imageUrl: `https://${configForFormat(format).targetHost}${ - upload.destinationPath - }`, + imageUrl, }, }, }); diff --git a/src/components/Card/CardContainer.tsx b/src/components/Card/CardContainer.tsx index 343eabd8..ddcb2d87 100644 --- a/src/components/Card/CardContainer.tsx +++ b/src/components/Card/CardContainer.tsx @@ -7,6 +7,7 @@ export function CardContainer( header?: ReactNode; additionalAction?: ReactNode; sx?: SxProps; + childSx?: SxProps; } & PropsWithChildren, ) { return ( @@ -23,7 +24,9 @@ export function CardContainer( )} - {props.children} + + {props.children} + ); diff --git a/src/components/Creatives/NotificationSelect.tsx b/src/components/Creatives/CreativeSelect.tsx similarity index 78% rename from src/components/Creatives/NotificationSelect.tsx rename to src/components/Creatives/CreativeSelect.tsx index 4b6d552d..cf31a639 100644 --- a/src/components/Creatives/NotificationSelect.tsx +++ b/src/components/Creatives/CreativeSelect.tsx @@ -8,8 +8,10 @@ import _ from "lodash"; import { useContext, useState } from "react"; import { FormContext } from "state/context"; import { useFormikContext } from "formik"; +import { CampaignFormat } from "graphql/types"; +import { ImagePreview } from "components/Assets/ImagePreview"; -export function NotificationSelect(props: { +export function CreativeSelect(props: { options: Creative[]; useSelectedAdStyle?: boolean; showState?: boolean; @@ -68,9 +70,9 @@ export function NotificationSelect(props: { } key={idx} > - {!(props.hideCreated ?? false) && ( @@ -109,3 +111,31 @@ export function NotificationSelect(props: { ); } + +const CreativeType = (props: { + creative: Creative; + format: CampaignFormat; + selected: boolean; +}) => { + const co = props.creative; + if (props.format === CampaignFormat.PushNotification) { + return ( + + ); + } else if (props.format === CampaignFormat.NewsDisplayAd) { + return ( + + ); + } + + return null; +}; diff --git a/src/components/Creatives/CreativeSpecificPreview.tsx b/src/components/Creatives/CreativeSpecificPreview.tsx index f094e8bd..7a34786e 100644 --- a/src/components/Creatives/CreativeSpecificPreview.tsx +++ b/src/components/Creatives/CreativeSpecificPreview.tsx @@ -6,6 +6,7 @@ import { PropsWithChildren } from "react"; import { useField } from "formik"; import { Creative } from "user/views/adsManager/types"; import { DisplayError } from "user/views/adsManager/views/advanced/components/review/components/ReviewField"; +import { ImagePreview } from "components/Assets/ImagePreview"; interface Props extends PropsWithChildren { options: Creative[]; @@ -31,6 +32,16 @@ export function CreativeSpecificPreview({ /> )); + } else if (format.value === CampaignFormat.NewsDisplayAd) { + component = options.map((c, idx) => ( + + + + )); } if (error) { diff --git a/src/components/Creatives/NewsPreview.tsx b/src/components/Creatives/NewsPreview.tsx index 04876250..b2956372 100644 --- a/src/components/Creatives/NewsPreview.tsx +++ b/src/components/Creatives/NewsPreview.tsx @@ -1,4 +1,4 @@ -import { Box, Card, Skeleton, Typography } from "@mui/material"; +import { Box, Card, Typography } from "@mui/material"; import { ImagePreview } from "components/Assets/ImagePreview"; import { useField } from "formik"; import { Creative } from "user/views/adsManager/types"; @@ -23,23 +23,17 @@ export function NewsPreview() { {value?.imageUrl ? ( ) : ( - - - + )} - {value?.title ?? + {value?.title || "This is a news display Ad, it wll look like part of the news feed."} - {value?.ctaText ?? "Click Here!"} + {value?.ctaText || "Click Here!"} diff --git a/src/user/ads/AdsExistingAd.tsx b/src/user/ads/AdsExistingAd.tsx index 4dc45a71..eeb0da26 100644 --- a/src/user/ads/AdsExistingAd.tsx +++ b/src/user/ads/AdsExistingAd.tsx @@ -19,7 +19,7 @@ import { CampaignForm } from "user/views/adsManager/types"; import { CardContainer } from "components/Card/CardContainer"; import SearchIcon from "@mui/icons-material/Search"; import { useContext, useRef, useState } from "react"; -import { NotificationSelect } from "components/Creatives/NotificationSelect"; +import { CreativeSelect } from "components/Creatives/CreativeSelect"; import { FormContext } from "state/context"; import { useAdvertiserCreatives } from "user/hooks/useAdvertiserCreatives"; @@ -129,7 +129,7 @@ const CreativeSpecificSelect = (props: { if (props.format === CampaignFormat.PushNotification) return ( - ({ ...o, advertiserId: advertiser.id, diff --git a/src/user/ads/InlineContentAd.tsx b/src/user/ads/InlineContentAd.tsx index fc4f8524..5d1b71f7 100644 --- a/src/user/ads/InlineContentAd.tsx +++ b/src/user/ads/InlineContentAd.tsx @@ -23,7 +23,7 @@ export function InlineContentAd() { return ( - + (); const { creatives } = useAdvertiserCreatives(); const [, , newCreative] = useField("newCreative"); const [, meta, helper] = useField("isCreating"); @@ -35,8 +41,16 @@ export function NewAd() { > { diff --git a/src/user/views/adsManager/views/advanced/components/adSet/fields/AdSetAds.tsx b/src/user/views/adsManager/views/advanced/components/adSet/fields/AdSetAds.tsx index 123e825f..42620d17 100644 --- a/src/user/views/adsManager/views/advanced/components/adSet/fields/AdSetAds.tsx +++ b/src/user/views/adsManager/views/advanced/components/adSet/fields/AdSetAds.tsx @@ -2,8 +2,7 @@ import { CardContainer } from "components/Card/CardContainer"; import { Typography } from "@mui/material"; import { CampaignForm } from "user/views/adsManager/types"; import { useFormikContext } from "formik"; -import { CampaignFormat } from "graphql/types"; -import { NotificationSelect } from "components/Creatives/NotificationSelect"; +import { CreativeSelect } from "components/Creatives/CreativeSelect"; interface Props { index: number; @@ -18,12 +17,7 @@ export function AdSetAds({ index }: Props) { Select the Ads you would like to include in this ad set. - {values.format === CampaignFormat.PushNotification && ( - - )} + ); } diff --git a/src/user/views/user/CampaignView.tsx b/src/user/views/user/CampaignView.tsx index a075ae76..7054a1f9 100644 --- a/src/user/views/user/CampaignView.tsx +++ b/src/user/views/user/CampaignView.tsx @@ -89,6 +89,10 @@ export function CampaignView() { } function CampaignHeader(props: { selectedCampaigns: string[] }) { + const editableCampaigns = [ + CampaignFormat.PushNotification, + CampaignFormat.NewsDisplayAd, + ]; const oneCampaignSelected = props.selectedCampaigns.length === 1; const firstCampaign = oneCampaignSelected ? props.selectedCampaigns[0] : null; const { data, loading } = useLoadCampaignQuery({ @@ -101,7 +105,7 @@ function CampaignHeader(props: { selectedCampaigns: string[] }) { if (!loading && data?.campaign) { isValidCampaign = data.campaign.source === CampaignSource.SelfServe && - data.campaign.format === CampaignFormat.PushNotification && + editableCampaigns.includes(data.campaign.format) && data.campaign.state !== "completed"; tooltip = isValidCampaign ? null : "Cannot edit this campaign"; }