Skip to content

Commit

Permalink
chore(platform): delete old images on upload of new ones (#2738)
Browse files Browse the repository at this point in the history
* chore(platform): delete old images on upload of new ones

* Add and delete images, update app details and
themes

* Fix image deletion bug in app designer and profile
page

* Fix profile image update bug

* Update variable name

* Added helper

* Refactored helper

* Extracted upload functionality away from icon component

* Add waitUntil function to AppLoadContext and fix
image upload in console app
  • Loading branch information
Cosmin-Parvulescu authored Nov 14, 2023
1 parent 62e4562 commit ef1fc0e
Show file tree
Hide file tree
Showing 11 changed files with 382 additions and 152 deletions.
114 changes: 18 additions & 96 deletions apps/console/app/components/IconPicker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,14 @@ import { CameraIcon } from '@heroicons/react/24/outline'
// -----------------------------------------------------------------------------

function pickIcon(
setIcon: React.Dispatch<React.SetStateAction<string>>,
setIconUrl: React.Dispatch<React.SetStateAction<string>>,
setLocalIconURL: React.Dispatch<React.SetStateAction<string>>,
maxImgSize = 1048576,
aspectRatio?: {
width: number
height: number
},
minWidth?: number,
minHeight?: number,
variant: string = 'public'
minHeight?: number
) {
return (e: any) =>
new Promise<any>(async (ok) => {
Expand Down Expand Up @@ -64,36 +62,9 @@ function pickIcon(
const iconFile = files.item(0)
const reader = new FileReader()
reader.onload = (e) => {
setIcon(e?.target?.result as string)
setLocalIconURL(e?.target?.result as string)
}
reader.readAsDataURL(iconFile)

const imgUploadUrl = (await fetch('/api/image-upload-url', {
method: 'post',
}).then((res) => {
return res.json()
})) as string

const formData = new FormData()
formData.append('file', iconFile)

const cfUploadRes: {
success: boolean
result: {
variants: string[]
}
} = await fetch(imgUploadUrl, {
method: 'POST',
body: formData,
}).then((res) => res.json())

const variantUrls = cfUploadRes.result.variants.filter((v) =>
v.endsWith(variant)
)

if (variantUrls.length) {
setIconUrl(variantUrls[0])
}
}

ok(errors)
Expand Down Expand Up @@ -145,67 +116,23 @@ export default function IconPicker({
setIsFormChanged,
setIsImgUploading,
imageUploadCallback = () => {},
variant,
variant = 'public',
}: IconPickerProps) {
const [icon, setIcon] = useState<string>('')
const [iconUrl, setIconUrl] = useState<string>('')
const [iconURL, setIconURL] = useState<string>('')
const [invalidState, setInvalidState] = useState(invalid)
const [errorMessageState, setErrorMessageState] = useState(errorMessage)

useEffect(() => {
setIconUrl(url !== undefined ? url : '')
setIcon(url !== undefined ? url : '')
setIconURL(url !== undefined ? url : '')
setInvalidState(undefined)
setErrorMessageState(undefined)
}, [url])

useEffect(() => {
if (!iconUrl) return

imageUploadCallback(iconUrl)
}, [iconUrl])

const handleDrop = async (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault()
e.stopPropagation()

const files = [...e.dataTransfer.files]
if (files && files.length > 0) {
const file = files.pop()
if (!file) return

// Ignore dropped files that aren't images.
if (!file.type.startsWith('image/')) {
return
}

const reader = new FileReader()
reader.onload = (e) => {
if (!e.target) return
// Set the data URL as the <img src="..."/> value.
setIcon(e.target.result as string)
}
// Read file as data URL, triggering onload handler.
reader.readAsDataURL(file)

e.dataTransfer.clearData()
}
}
if (!iconURL) return

const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault()
e.stopPropagation()
}

const handleDragEnter = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault()
e.stopPropagation()
}

const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault()
e.stopPropagation()
}
imageUploadCallback(iconURL)
}, [iconURL])

const calculateDimensions = (
aspectRatioWidth: number,
Expand Down Expand Up @@ -235,7 +162,6 @@ export default function IconPicker({
{label && (
<label className="text-sm font-medium text-gray-700">{label}</label>
)}
{id && <input type="hidden" name={id} value={iconUrl} />}
<div className="flex flex-col md:flex-row md:gap-4 items-center">
<div className="flex flex-row gap-4">
<div
Expand All @@ -244,17 +170,13 @@ export default function IconPicker({
width: `${width}px`,
height: `${height}px`,
backgroundImage:
iconUrl && iconUrl !== '' ? `url(${iconUrl})` : '',
iconURL && iconURL !== '' ? `url(${iconURL})` : '',
backgroundSize: 'contain',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center',
}}
onDrop={(e) => handleDrop(e)}
onDragOver={(e) => handleDragOver(e)}
onDragEnter={(e) => handleDragEnter(e)}
onDragLeave={(e) => handleDragLeave(e)}
>
{(!iconUrl || iconUrl === '') && (
{(!iconURL || iconURL === '') && (
<CameraIcon
className="h-6 w-6 text-gray-300"
aria-hidden="true"
Expand All @@ -264,7 +186,7 @@ export default function IconPicker({

<div className="grid place-items-center">
<label
htmlFor="icon-upload"
htmlFor={`${id}_file`}
className={`rounded bg-transparent text-sm border
py-2 px-4 hover:bg-gray-100
focus:bg-indigo-400 hover:cursor-pointer
Expand All @@ -275,22 +197,22 @@ export default function IconPicker({
</Text>
<input
type="file"
id="icon-upload"
name="icon"
id={`${id}_file`}
name={`${id}_file`}
data-variant={variant}
data-name={id}
accept="image/png,image/jpeg,image/gif,image/webp"
className="sr-only"
onChange={async (event) => {
event.stopPropagation()
setIsFormChanged(false)
setIsImgUploading(true)
const errors = await pickIcon(
setIcon,
setIconUrl,
setIconURL,
maxSize,
aspectRatio,
minWidth,
minHeight,
variant
minHeight
)(event)
if (Object.keys(errors).length) {
setInvalidState(true)
Expand Down
21 changes: 20 additions & 1 deletion apps/console/app/routes/apps/$clientId/auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ import type { notificationHandlerType } from '~/types'
import { SCOPE_SMART_CONTRACT_WALLETS } from '@proofzero/security/scopes'
import { BadRequestError } from '@proofzero/errors'
import { getRollupReqFunctionErrorWrapper } from '@proofzero/utils/errors'
import createImageClient from '@proofzero/platform-clients/image'
import { captureFormSubmitAndReplaceImages } from '~/utils/formCFImages.client'

/**
* @file app/routes/dashboard/index.tsx
Expand Down Expand Up @@ -171,6 +173,10 @@ export const action: ActionFunction = getRollupReqFunctionErrorWrapper(
).secret
break
case 'update_app':
const appDetails = await coreClient.starbase.getAppDetails.query({
clientId: params.clientId,
})

const entries = formData.entries()
const scopes = Array.from(entries)
.filter((entry) => {
Expand Down Expand Up @@ -203,6 +209,8 @@ export const action: ActionFunction = getRollupReqFunctionErrorWrapper(
}

if (Object.keys(errors).length === 0) {
const oauthLogo = appDetails.app?.icon

await Promise.all([
coreClient.starbase.updateApp.mutate({
clientId: params.clientId,
Expand All @@ -213,6 +221,14 @@ export const action: ActionFunction = getRollupReqFunctionErrorWrapper(
published: published,
}),
])

if (oauthLogo && oauthLogo !== updates.icon) {
const imageClient = createImageClient(context.env.Images, {
headers: generateTraceContextHeaders(context.traceSpan),
})

context.waitUntil(imageClient.delete.mutate(oauthLogo))
}
}
break
}
Expand Down Expand Up @@ -319,6 +335,9 @@ export default function AppDetailIndexPage() {
onChange={() => {
setIsFormChanged(true)
}}
onSubmitCapture={(event) =>
captureFormSubmitAndReplaceImages(event, submit, setIsImgUploading)
}
>
<fieldset disabled={isImgUploading}>
<input type="hidden" name="op" value="update_app" />
Expand All @@ -336,7 +355,7 @@ export default function AppDetailIndexPage() {
<Button
type="submit"
btnType="primary-alt"
disabled={!isFormChanged}
disabled={!isFormChanged || isImgUploading}
>
Save
</Button>
Expand Down
43 changes: 41 additions & 2 deletions apps/console/app/routes/apps/$clientId/designer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
useFetcher,
useLoaderData,
useOutletContext,
useSubmit,
} from '@remix-run/react'
import {
ReactNode,
Expand Down Expand Up @@ -56,7 +57,12 @@ import {
OGTheme,
OGThemeSchema,
} from '@proofzero/platform/starbase/src/jsonrpc/validators/app'
import { ActionFunction, LoaderFunction, json } from '@remix-run/cloudflare'
import {
ActionFunction,
AppLoadContext,
LoaderFunction,
json,
} from '@remix-run/cloudflare'
import { requireJWT } from '~/utilities/session.server'
import { generateTraceContextHeaders } from '@proofzero/platform-middleware/trace'
import createCoreClient from '@proofzero/platform-clients/core'
Expand Down Expand Up @@ -97,6 +103,8 @@ import designerSVG from '~/assets/early/designer.webp'
import EarlyAccessPanel from '~/components/EarlyAccess/EarlyAccessPanel'
import { IdentityURN } from '@proofzero/urns/identity'
import { GetOgThemeResult } from '@proofzero/platform.starbase/src/jsonrpc/methods/getOgTheme'
import createImageClient from '@proofzero/platform-clients/image'
import { captureFormSubmitAndReplaceImages } from '~/utils/formCFImages.client'

const LazyAuth = lazy(() =>
// @ts-ignore :(
Expand Down Expand Up @@ -1567,6 +1575,8 @@ export const action: ActionFunction = getRollupReqFunctionErrorWrapper(
let colorDark = fd.get('colordark') as string | undefined
if (!colorDark || colorDark === '') colorDark = undefined

const ogGraphicURL = theme?.graphicURL

let graphicURL = fd.get('image') as string | undefined
if (!graphicURL || graphicURL === '') graphicURL = undefined

Expand Down Expand Up @@ -1607,6 +1617,8 @@ export const action: ActionFunction = getRollupReqFunctionErrorWrapper(
clientId,
theme,
})

await deleteUpdatedImage(context, ogGraphicURL, graphicURL)
}

return json({
Expand All @@ -1615,6 +1627,7 @@ export const action: ActionFunction = getRollupReqFunctionErrorWrapper(
}

const updateEmail = async (fd: FormData, theme: EmailOTPTheme) => {
const ogLogoURL = theme?.logoURL
let logoURL = fd.get('logoURL') as string | undefined
if (!logoURL || logoURL === '') logoURL = undefined

Expand Down Expand Up @@ -1647,6 +1660,8 @@ export const action: ActionFunction = getRollupReqFunctionErrorWrapper(
clientId,
theme,
})

await deleteUpdatedImage(context, ogLogoURL, logoURL)
}

return json({
Expand All @@ -1661,6 +1676,7 @@ export const action: ActionFunction = getRollupReqFunctionErrorWrapper(
let description = fd.get('ogDescription') as string | undefined
if (!description || description === '') description = undefined

const ogImageURL = theme?.image
let image = fd.get('ogImage') as string | undefined
if (!image || image === '') image = undefined

Expand All @@ -1687,6 +1703,8 @@ export const action: ActionFunction = getRollupReqFunctionErrorWrapper(
clientId,
theme,
})

await deleteUpdatedImage(context, ogImageURL, image)
}

return json({
Expand Down Expand Up @@ -1770,11 +1788,18 @@ export default () => {
)
}

const submit = useSubmit()

return (
<Suspense fallback={<Loader />}>
{loading && <Loader />}

<Form method="post">
<Form
method="post"
onSubmitCapture={(event) =>
captureFormSubmitAndReplaceImages(event, submit, setLoading)
}
>
<section className="flex flex-col lg:flex-row items-center justify-between mb-11">
<div className="flex flex-row items-center space-x-3">
<Text
Expand Down Expand Up @@ -1872,3 +1897,17 @@ export default () => {
</Suspense>
)
}

const deleteUpdatedImage = async (
context: AppLoadContext,
previousURL: string | undefined,
newURL: string | undefined
) => {
if (previousURL && previousURL !== newURL) {
const imageClient = createImageClient(context.env.Images, {
headers: generateTraceContextHeaders(context.traceSpan),
})

context.waitUntil(imageClient.delete.mutate(previousURL))
}
}
Loading

0 comments on commit ef1fc0e

Please sign in to comment.