Skip to content

Commit

Permalink
feat: add image upload controls
Browse files Browse the repository at this point in the history
  • Loading branch information
IanKrieger committed Aug 30, 2023
1 parent b0a9b23 commit faf0e1a
Show file tree
Hide file tree
Showing 12 changed files with 306 additions and 23 deletions.
65 changes: 65 additions & 0 deletions src/components/Assets/AdvertiserAssets.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {
AdvertiserImageFragment,
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 MiniSideBar from "components/Drawer/MiniSideBar";
import { UploadImage } from "components/Assets/UploadImage";
import { ImagePreview } from "components/Assets/ImagePreview";

export function AdvertiserAssets() {
const { advertiser } = useAdvertiser();
const { data, loading, error } = useAdvertiserImagesQuery({
variables: { id: advertiser.id },
});

const options: ColumnDescriptor<AdvertiserImageFragment>[] = [
{
title: "Created",
value: (c) => c.createdAt,
renderer: StandardRenderers.date,
},
{
title: "Name",
value: (c) => c.name,
},
{
title: "Image",
value: (c) => c.imageUrl,
extendedRenderer: (r) => <ImagePreview url={r.imageUrl} />,
},
];

return (
<MiniSideBar>
<CardContainer header="Assets" sx={{ minWidth: "50%" }}>
{loading && (
<Box m={3}>
<Skeleton variant="rounded" height={500} />
</Box>
)}
{error && (
<ErrorDetail
error={error}
additionalDetails="Unable to retrieve images"
/>
)}
{!loading && !error && (
<EnhancedTable
rows={data?.advertiser?.images ?? []}
columns={options}
/>
)}
</CardContainer>
<UploadImage />
</MiniSideBar>
);
}
47 changes: 47 additions & 0 deletions src/components/Assets/ImageAutocomplete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { useAdvertiserImagesQuery } from "graphql/advertiser.generated";
import { useAdvertiser } from "auth/hooks/queries/useAdvertiser";
import { Autocomplete, TextField } from "@mui/material";
import { useState } from "react";
import { useField } from "formik";

export function ImageAutocomplete() {
const [, meta, imageUrl] = useField(
`newCreative.payloadInlineContent.imageUrl`,
);
const hasError = Boolean(meta.error);
const showError = hasError && meta.touched;
const { advertiser } = useAdvertiser();
const [options, setOptions] = useState<{ label: string; image: string }[]>();
const { loading } = useAdvertiserImagesQuery({
variables: { id: advertiser.id },
onCompleted(data) {
const images = data.advertiser?.images ?? [];
const options = images.map((i) => ({
label: i.name,
image: i.imageUrl,
}));

setOptions(options);
},
});

return (
<Autocomplete
disablePortal
loading={!options || loading}
options={options ?? []}
renderInput={(params) => (
<TextField
{...params}
label="Image"
helperText={showError ? meta.error : undefined}
error={showError}
margin="normal"
/>
)}
onChange={(e, nv) => {
imageUrl.setValue(nv!.image);
}}
/>
);
}
44 changes: 44 additions & 0 deletions src/components/Assets/ImagePreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Box, LinearProgress, Link } from "@mui/material";
import { useGetImagePreviewUrl } from "components/Assets/hooks/useGetImagePreviewUrl";
import { ErrorDetail } from "components/Error/ErrorDetail";

interface Props {
url: string;
height?: number;
width?: number;
}

export const ImagePreview = ({ url, height = 300, width = 360 }: Props) => {
const { data, loading, error } = useGetImagePreviewUrl({ url });

if (!data || loading) {
return <LinearProgress />;
}

if (error) {
return <ErrorDetail error={error} />;
}

return (
<Box
sx={{
height,
width,
bgcolor: "#AEB1C2",
}}
>
{url?.endsWith(".pad") ? (
<img height={height} width={width} src={data} alt="preview" />
) : (
<Link underline="none" href={url} target="_blank" rel="noreferrer">
<img
height={height}
width={width}
src={data}
alt="Image failed to load"
/>
</Link>
)}
</Box>
);
};
1 change: 1 addition & 0 deletions src/components/Assets/NewImageButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export function NewImageButton() {}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useState } from "react";
import { useUploadFile } from "user/hooks/useUploadFile";
import {
Alert,
Box,
Button,
Chip,
Dialog,
DialogActions,
DialogContent,
Expand All @@ -15,6 +15,7 @@ import {
Stepper,
} from "@mui/material";
import { CampaignFormat } from "graphql/types";
import { useUploadFile } from "components/Assets/hooks/useUploadFile";

export interface UploadConfig {
targetHost: () => string;
Expand All @@ -25,7 +26,7 @@ export interface UploadConfig {
export function UploadImage() {
const [open, setOpen] = useState(false);
const [file, setFile] = useState<File>();
const [{ upload }, { step, error, loading, state }] = useUploadFile();
const [{ upload, reset }, { step, error, loading, state }] = useUploadFile();

return (
<Box>
Expand All @@ -39,7 +40,7 @@ export function UploadImage() {
Images will be automatically scaled to this size.
</DialogContentText>

<Stepper activeStep={step}>
<Stepper activeStep={step} sx={{ mt: 3 }}>
<Step>
<StepLabel>Choose</StepLabel>
</Step>
Expand All @@ -51,8 +52,8 @@ export function UploadImage() {
</Step>
</Stepper>

<Box my={2} height={80} width={400}>
{step === 0 && (
<Box mt={3}>
{step === 0 && file === undefined && (
<Button
variant="contained"
component="label"
Expand All @@ -67,31 +68,39 @@ export function UploadImage() {
/>
</Button>
)}
{step === 0 && !!file && (
<Chip onDelete={() => setFile(undefined)} label={file.name} />
)}

{!error && state && (
<Alert severity={step !== 2 ? "info" : "success"}>{state}</Alert>
)}
{error !== undefined && <Alert severity="error">{error}</Alert>}
{loading && <LinearProgress />}
{loading && <LinearProgress sx={{ mt: 1 }} />}
</Box>
</DialogContent>
<DialogActions>
<Button
disabled={file === undefined}
onClick={() => {
upload!(file!, CampaignFormat.NewsDisplayAd);
}}
>
Upload
</Button>
<Button
onClick={() => {
setFile(undefined);
setOpen(false);
setFile(undefined);
reset!();
}}
variant="outlined"
>
Cancel
{step === 2 ? "Close" : "Cancel"}
</Button>
{step !== 2 && (
<Button
disabled={file === undefined}
onClick={() => {
upload!(file!, CampaignFormat.NewsDisplayAd);
}}
variant="contained"
>
Upload
</Button>
)}
</DialogActions>
</Dialog>
</Box>
Expand Down
82 changes: 82 additions & 0 deletions src/components/Assets/hooks/useGetImagePreviewUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (c) 2020 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// you can obtain one at http://mozilla.org/MPL/2.0/.

// FROM: https://github.com/brave/brave-core/blob/976e81322aab22de9bb3670f2cec23da76a1600f/components/brave_extension/extension/brave_extension/background/today/privateCDN.ts

import { useEffect, useMemo, useState } from "react";

export function useGetImagePreviewUrl(props: { url: string }) {
const [data, setData] = useState<string>();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string>();

const fetchImageResource = useMemo(async () => {
const result = await fetchResource(props.url);
if (!result.ok) {
throw new Error("Unable to fetch image");
}

return await result.arrayBuffer();
}, [props.url]);

useEffect(() => {
async function fetchImage(url: string) {
if (!url.endsWith(".pad")) {
setData(url);
return;
}
setLoading(true);

let blob;
try {
blob = await fetchImageResource;
} catch (e: any) {
setError(e.message);
return;
}

getUnpaddedAsDataUrl(blob)
.then((res) => {
setData(res);
})
.finally(() => {
setLoading(false);
});
}

fetchImage(props.url);
}, [props.url]);

return { data, loading, error };
}

async function fetchResource(url: string): Promise<Response> {
try {
const response = await fetch(url, {
headers: new Headers({
"Accept-Language": "*",
}),
cache: "no-cache",
});
return response;
} catch (e) {
return new Response(null, { status: 502 });
}
}

async function getUnpaddedAsDataUrl(
buffer: ArrayBuffer,
mimeType = "image/jpg",
): Promise<string> {
const data = new DataView(buffer);
const contentLength = data.getUint32(0, false /* big endian */);
const unpaddedData = buffer.slice(4, contentLength + 4);
const unpaddedBlob = new Blob([unpaddedData], { type: mimeType });
return await new Promise<string>((resolve) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.readAsDataURL(unpaddedBlob);
});
}
Loading

0 comments on commit faf0e1a

Please sign in to comment.