Skip to content

Commit

Permalink
Frontend cloudinary (#1181)
Browse files Browse the repository at this point in the history
* add cloudinary example. fix bugs

* use upload widget in form

* save cloudinary to db from form

* use any to stop cloudinary from breaking

* declare cloudinary types

* remove unused page

* remove unused packages. use useCallback in upload widget
  • Loading branch information
stormcloud266 authored Apr 11, 2024
1 parent dc4bb57 commit 4bd9f32
Show file tree
Hide file tree
Showing 9 changed files with 538 additions and 359 deletions.
2 changes: 1 addition & 1 deletion api/src/graphql/rewardables.sdl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const schema = gql`
input FormNftEditInput {
name: String!
image: String
image: String!
}
enum RewardableType {
Expand Down
87 changes: 0 additions & 87 deletions api/src/lib/puzzleForm.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { CLOUDINARY_CLOUD_NAME } from '@infinity-keys/constants'
import { StepGuideType, StepType } from '@prisma/client'
import { v2 as cloudinary } from 'cloudinary'
import buildUrl from 'cloudinary-build-url'
import { nanoid } from 'nanoid'
import slugify from 'slugify'
import { CreateStepInput } from 'types/graphql'

import { db } from 'src/lib/db'

// Formats steps coming from the form into the shape expected by Prisma `create`
export const formatCreateSteps = (steps: CreateStepInput[]) => {
const formattedSteps = steps.map(
Expand Down Expand Up @@ -144,90 +141,6 @@ export const generateNftImage = (id: string) => {
})
}

/**
* Returns new NFT update object only if the NFT has been editing from the form.
* Skips uploading to Cloudinary unless image has changed.
* The NFT's metadata field is a JSON object, so it needs to be entirely
* re-written every time a field changes.
*/
export const getOptionalNftUpdateValues = async ({
newName,
newImage,
rewardableId,
slug,
}: {
newName?: string
newImage?: string
rewardableId: string
slug: string
}) => {
if (!newImage && !newName) {
return
}

cloudinary.config({ secure: true })

const cloudinaryRes =
newImage &&
(await cloudinary.uploader.upload(newImage, {
use_filename: false,
unique_filename: true,
folder: 'ik-alpha-creators',
}))

if (newImage && (!cloudinaryRes || !cloudinaryRes?.public_id)) {
throw new Error('There was a problem uploading NFT image to Cloudinary')
}

const prevNftData = await db.nft.findFirst({
where: {
rewardables: {
some: {
id: rewardableId,
},
},
},
})

// Get previous values from the Prisma JSON object as fallback for unedited fields
const prevMetadata = prevNftData?.data
const prevName =
prevMetadata &&
typeof prevMetadata === 'object' &&
'name' in prevMetadata &&
typeof prevMetadata.name === 'string' &&
prevMetadata.name

const prevImage =
prevMetadata &&
typeof prevMetadata === 'object' &&
'image' in prevMetadata &&
prevMetadata.image

if (!prevImage || !prevName) {
throw new Error("There was a problem obtaining the NFT's previous metadata")
}

return {
cloudinaryId:
cloudinaryRes && cloudinaryRes.public_id
? cloudinaryRes.public_id
: prevNftData?.cloudinaryId,
data: {
name: newName || prevName,
image:
cloudinaryRes && cloudinaryRes?.public_id
? generateNftImage(cloudinaryRes.public_id)
: prevImage,
description: generateNftDescription({
slug,
name: newName || prevName,
}),
external_url: `https://www.infinitykeys.io/puzzle/${slug}`,
},
}
}

export const generateSlug = (name: string): string => {
return slugify(`${name}-${nanoid()}`, {
strict: true,
Expand Down
52 changes: 20 additions & 32 deletions api/src/services/ik/rewardablePuzzle/rewardablePuzzleService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
generateNftDescription,
generateNftImage,
generateSlug,
getOptionalNftUpdateValues,
isAlphanumeric,
} from 'src/lib/puzzleForm'
import { getNftData } from 'src/lib/web3/get-nft-data'
Expand Down Expand Up @@ -56,14 +55,6 @@ export const editRewardablePuzzle: MutationResolvers['editRewardablePuzzle'] =
? prevRewardable.slug
: generateSlug(input.name)

// Handle optional Cloudinary upload
const nftUpdateData = await getOptionalNftUpdateValues({
newName: input.nft.name,
newImage: input.nft.image ?? undefined,
slug,
rewardableId,
})

const deleteStepsOperation = db.step.deleteMany({
where: { puzzleId },
})
Expand Down Expand Up @@ -91,18 +82,25 @@ export const editRewardablePuzzle: MutationResolvers['editRewardablePuzzle'] =
},
},
},
...(nftUpdateData
? {
nfts: {
// @NOTE: Rewardables can have multiple NFTs, but currently we only
// support one. Be sure to update this if that changes.
updateMany: {
where: {},
data: nftUpdateData,
},
nfts: {
// @NOTE: Rewardables can have multiple NFTs, but currently we only
// support one. Be sure to update this if that changes.
updateMany: {
where: {},
data: {
cloudinaryId: input.nft.image,
data: {
name: input.nft.name,
image: generateNftImage(input.nft.image),
description: generateNftDescription({
slug,
name: input.nft.name,
}),
external_url: `https://www.infinitykeys.io/puzzle/${slug}`,
},
}
: {}),
},
},
},
},
})

Expand Down Expand Up @@ -215,16 +213,6 @@ export const createRewardablePuzzle: MutationResolvers['createRewardablePuzzle']
throw new Error('There was a problem obtaining org id')
}

const result = await cloudinary.uploader.upload(input.nft.image, {
use_filename: false,
unique_filename: true,
folder: 'ik-alpha-creators',
})

if (!result.public_id) {
throw new Error('There was a problem uploading NFT image to Cloudinary')
}

const { tokenId, lookupId } = await getNftData()
const slug = generateSlug(input.name)

Expand Down Expand Up @@ -254,10 +242,10 @@ export const createRewardablePuzzle: MutationResolvers['createRewardablePuzzle']
tokenId,
lookupId,
contractName: 'achievement',
cloudinaryId: result.public_id,
cloudinaryId: input.nft.image,
data: {
name: input.nft.name,
image: generateNftImage(result.public_id),
image: generateNftImage(input.nft.image),
attributes: [
{
value: 'Community',
Expand Down
10 changes: 2 additions & 8 deletions web/src/components/EditRewardableCell/EditRewardableCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const QUERY = gql`
name
listPublicly
nfts {
cloudinaryId
data
}
puzzle {
Expand Down Expand Up @@ -142,13 +143,6 @@ export const Success = ({
typeof dbNft.name === 'string'
? dbNft.name
: ''
const nftImage =
dbNft &&
typeof dbNft === 'object' &&
'image' in dbNft &&
typeof dbNft.image === 'string'
? dbNft.image
: ''

return (
<PuzzleForm
Expand All @@ -158,7 +152,7 @@ export const Success = ({
listPublicly: rewardable.listPublicly,
nft: {
name: nftName,
image: nftImage,
image: rewardable.nfts[0]?.cloudinaryId || '',
},
},
puzzle: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { useCallback, useEffect, useRef, useState } from 'react'

import CloudArrowUpIcon from '@heroicons/react/20/solid/CloudArrowUpIcon'
import XCircleIcon from '@heroicons/react/20/solid/XCircleIcon'
import { CLOUDINARY_CLOUD_NAME } from '@infinity-keys/constants'
import { cloudinaryUrl } from '@infinity-keys/core'

import Button from 'src/components/Button'

import DisplayImage from '../DisplayImage/DisplayImage'

export const formatImageSrc = (src: string) => {
if (src.startsWith('https')) return src

return cloudinaryUrl(src, 300, 300, false, 1)
}

const CloudinaryUploadWidget = ({
nftImage,
setNftImage,
}: {
nftImage?: string
setNftImage: (s: string) => void
}) => {
const [loaded, setLoaded] = useState(false)
const uploadWidget = useRef<cloudinary.WidgetInterface | null>(null)

useEffect(() => {
// Check if the script is already loaded
if (!loaded) {
const uwScript = document.getElementById('uw')
if (!uwScript) {
// If not loaded, create and load the script
const script = document.createElement('script')
script.setAttribute('async', '')
script.setAttribute('id', 'uw')
script.src = 'https://upload-widget.cloudinary.com/global/all.js'
script.addEventListener('load', () => setLoaded(true))
document.body.appendChild(script)
} else {
// If already loaded, update the state
setLoaded(true)
}
}
}, [loaded])

const initializeCloudinaryWidget = useCallback(() => {
if (loaded) {
if (!uploadWidget.current && window) {
uploadWidget.current = window.cloudinary.createUploadWidget(
uploadOptions,
(error, result) => {
if (!error && result && result.event === 'success') {
const info =
result.info as cloudinary.CloudinaryEventInfoMap['success']
setNftImage(info.public_id)
}
}
)
}

if (uploadWidget.current) {
uploadWidget.current.open()
}
}
}, [loaded, setNftImage])

return (
<>
{nftImage ? (
<div className="relative mb-6 inline-flex">
<DisplayImage src={formatImageSrc(nftImage)} />
<button
type="button"
className="absolute top-0 right-0 translate-x-3 -translate-y-3 shadow-md"
onClick={() => setNftImage('')}
>
<XCircleIcon className="h-6 w-6" />
</button>
</div>
) : (
<Button
type="button"
borderWhite
round
onClick={initializeCloudinaryWidget}
disabled={!loaded}
>
<span className="flex items-center gap-2 text-xs">
<CloudArrowUpIcon className="h-5 w-5" /> Upload Image
</span>
</Button>
)}
</>
)
}

export default CloudinaryUploadWidget

const sources: cloudinary.Sources[] = ['local', 'url', 'camera']

const uploadOptions = {
cloudName: CLOUDINARY_CLOUD_NAME,
uploadPreset: 'ml_default',
cropping: true,
maxImageFileSize: 5000000, // Restrict file size to less than 5MB
maxImageWidth: 1000, // Scales the image down to a width of 1000 pixels before uploading
folder: 'ik-alpha-creators',
sources,
// Upload modal styles
styles: {
palette: {
window: '#1E1E1C',
sourceBg: '#1E1E1C',
windowBorder: '#c7a49f',
tabIcon: '#F1C391',
inactiveTabIcon: '#E8D5BB',
menuIcons: '#ebe5db',
link: '#F1C391',
action: '#F1C391',
inProgress: '#99cccc',
complete: '#78b3b4',
error: '#ff6666',
textDark: '#1E1E1C',
textLight: '#D8CFCF',
},
fonts: {
default: null,
"'Poppins', sans-serif": {
url: 'https://fonts.googleapis.com/css?family=Poppins',
active: true,
},
},
},
}
Loading

0 comments on commit 4bd9f32

Please sign in to comment.