Get image upload to cloudinary with the same public id
This means we can put a consistent url in the head for the card image
This commit is contained in:
@@ -4,8 +4,19 @@ import Popover from '@material-ui/core/Popover'
|
||||
import Svg from 'src/components/Svg/Svg'
|
||||
import Button from 'src/components/Button/Button'
|
||||
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
|
||||
import { canvasToBlob, blobTo64 } from 'src/helpers/canvasToBlob'
|
||||
import { useUpdateProject } from 'src/helpers/hooks/useUpdateProject'
|
||||
import { uploadToCloudinary } from 'src/helpers/cloudinary'
|
||||
import {
|
||||
useUpdateSocialCard,
|
||||
makeSocialPublicId,
|
||||
} from 'src/helpers/hooks/useUpdateSocialCard'
|
||||
import {
|
||||
uploadToCloudinary,
|
||||
serverVerifiedImageUpload,
|
||||
} from 'src/helpers/cloudinary'
|
||||
import SocialCardCell from 'src/components/SocialCardCell/SocialCardCell'
|
||||
import { toJpeg } from 'html-to-image'
|
||||
import { useAuth } from '@redwoodjs/auth'
|
||||
|
||||
const anchorOrigin = {
|
||||
vertical: 'bottom',
|
||||
@@ -16,51 +27,50 @@ const transformOrigin = {
|
||||
horizontal: 'center',
|
||||
}
|
||||
|
||||
const CaptureButton = ({ canEdit, TheButton, shouldUpdateImage }) => {
|
||||
const CaptureButton = ({
|
||||
canEdit,
|
||||
TheButton,
|
||||
shouldUpdateImage,
|
||||
projectTitle,
|
||||
userName,
|
||||
}) => {
|
||||
const [captureState, setCaptureState] = useState<any>({})
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
const [whichPopup, setWhichPopup] = useState(null)
|
||||
const { state, project } = useIdeContext()
|
||||
const ref = React.useRef<HTMLDivElement>(null)
|
||||
const { updateProject } = useUpdateProject({
|
||||
onCompleted: () => toast.success('Image updated'),
|
||||
})
|
||||
const { updateSocialCard } = useUpdateSocialCard({})
|
||||
const { getToken } = useAuth()
|
||||
|
||||
const getSocialBlob = async (): Promise<string> => {
|
||||
const tokenPromise = getToken()
|
||||
const blob = await toJpeg(ref.current, { cacheBust: true, quality: 0.75 })
|
||||
const token = await tokenPromise
|
||||
const { publicId } = await serverVerifiedImageUpload(
|
||||
blob,
|
||||
project?.id,
|
||||
token,
|
||||
makeSocialPublicId(userName, projectTitle)
|
||||
)
|
||||
return publicId
|
||||
}
|
||||
|
||||
const onCapture = async () => {
|
||||
const threeInstance = state.threeInstance
|
||||
const isOpenScadImage = state?.objectData?.type === 'png'
|
||||
let imgBlob
|
||||
let image64
|
||||
if (!isOpenScadImage) {
|
||||
const updateCanvasSize = ({
|
||||
width,
|
||||
height,
|
||||
}: {
|
||||
width: number
|
||||
height: number
|
||||
}) => {
|
||||
threeInstance.camera.aspect = width / height
|
||||
threeInstance.camera.updateProjectionMatrix()
|
||||
threeInstance.gl.setSize(width, height)
|
||||
threeInstance.gl.render(
|
||||
threeInstance.scene,
|
||||
threeInstance.camera,
|
||||
null,
|
||||
false
|
||||
)
|
||||
}
|
||||
const oldSize = threeInstance.size
|
||||
updateCanvasSize({ width: 400, height: 300 })
|
||||
imgBlob = new Promise((resolve, reject) => {
|
||||
threeInstance.gl.domElement.toBlob(
|
||||
(blob) => {
|
||||
resolve(blob)
|
||||
},
|
||||
'image/jpeg',
|
||||
1
|
||||
)
|
||||
})
|
||||
updateCanvasSize(oldSize)
|
||||
imgBlob = canvasToBlob(threeInstance, { width: 400, height: 300 })
|
||||
image64 = blobTo64(
|
||||
await canvasToBlob(threeInstance, { width: 500, height: 522 })
|
||||
)
|
||||
} else {
|
||||
imgBlob = state.objectData.data
|
||||
image64 = blobTo64(state.objectData.data)
|
||||
}
|
||||
const config = {
|
||||
image: await imgBlob,
|
||||
@@ -69,11 +79,22 @@ const CaptureButton = ({ canEdit, TheButton, shouldUpdateImage }) => {
|
||||
callback: uploadAndUpdateImage,
|
||||
cloudinaryImgURL: '',
|
||||
updated: false,
|
||||
image64: await image64,
|
||||
}
|
||||
setCaptureState(config)
|
||||
|
||||
async function uploadAndUpdateImage() {
|
||||
// Upload the image to Cloudinary
|
||||
const cloudinaryImgURL = await uploadToCloudinary(config.image)
|
||||
const [cloudinaryImgURL, socialCloudinaryURL] = await Promise.all([
|
||||
uploadToCloudinary(config.image),
|
||||
getSocialBlob(),
|
||||
])
|
||||
|
||||
updateSocialCard({
|
||||
variables: {
|
||||
projectId: project?.id,
|
||||
url: socialCloudinaryURL,
|
||||
},
|
||||
})
|
||||
|
||||
// Save the screenshot as the mainImage
|
||||
updateProject({
|
||||
@@ -92,9 +113,8 @@ const CaptureButton = ({ canEdit, TheButton, shouldUpdateImage }) => {
|
||||
if (shouldUpdateImage) {
|
||||
config.cloudinaryImgURL = (await uploadAndUpdateImage()).public_id
|
||||
config.updated = true
|
||||
setCaptureState(config)
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
const handleDownload = (url) => {
|
||||
@@ -123,7 +143,7 @@ const CaptureButton = ({ canEdit, TheButton, shouldUpdateImage }) => {
|
||||
<TheButton
|
||||
onClick={async (event) => {
|
||||
handleClick({ event, whichPopup: 'capture' })
|
||||
setCaptureState(await onCapture())
|
||||
onCapture()
|
||||
}}
|
||||
/>
|
||||
<Popover
|
||||
@@ -163,7 +183,7 @@ const CaptureButton = ({ canEdit, TheButton, shouldUpdateImage }) => {
|
||||
name="refresh"
|
||||
className="mr-2 w-4 text-indigo-600"
|
||||
/>{' '}
|
||||
Update Part Image
|
||||
Update Project Image
|
||||
</button>
|
||||
) : (
|
||||
<div className="flex justify-center mb-4">
|
||||
@@ -187,6 +207,20 @@ const CaptureButton = ({ canEdit, TheButton, shouldUpdateImage }) => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="rounded-lg shadow-md mt-4 overflow-hidden">
|
||||
<div
|
||||
className="transform scale-50 origin-top-left"
|
||||
style={{ width: '600px', height: '315px' }}
|
||||
>
|
||||
<div style={{ width: '1200px', height: '630px' }} ref={ref}>
|
||||
<SocialCardCell
|
||||
userName={userName}
|
||||
projectTitle={projectTitle}
|
||||
image64={captureState.image64}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
@@ -37,12 +37,13 @@ export const Failure = ({ error }: CellFailureProps) => (
|
||||
|
||||
export const Success = ({
|
||||
userProject,
|
||||
variables: { image64 },
|
||||
}: CellSuccessProps<FindSocialCardQuery>) => {
|
||||
const image = userProject?.Project?.mainImage
|
||||
const gravatar = userProject?.image
|
||||
return (
|
||||
<div
|
||||
className="flex-col flex h-screen bg-ch-gray-800 text-ch-gray-300"
|
||||
className="flex-col flex h-full bg-ch-gray-800 text-ch-gray-300"
|
||||
id="social-card-loaded"
|
||||
>
|
||||
<div
|
||||
@@ -65,7 +66,7 @@ export const Success = ({
|
||||
{gravatar && (
|
||||
<Gravatar image={gravatar} className="w-14 h-14" size={60} />
|
||||
)}
|
||||
<div className="text-2xl font-fira-sans ml-6">
|
||||
<div className="text-2xl font-fira-sans ml-6 whitespace-nowrap">
|
||||
{userProject?.userName}
|
||||
</div>
|
||||
</div>
|
||||
@@ -86,13 +87,19 @@ export const Success = ({
|
||||
</div>
|
||||
<div className="h-full overflow-hidden relative">
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<CloudinaryImage
|
||||
cloudName="irevdev"
|
||||
publicId={image || 'CadHub/eia1kwru54g2kf02s2xx'}
|
||||
width={500}
|
||||
height={522}
|
||||
crop="crop"
|
||||
/>
|
||||
{image64 ? (
|
||||
<div
|
||||
style={{ backgroundImage: `url(${image64})` }}
|
||||
className="w-full h-full bg-no-repeat bg-center"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
backgroundImage: `url(http://res.cloudinary.com/irevdev/image/upload/c_crop,h_522,w_500/v1/${image})`,
|
||||
}}
|
||||
className="w-full h-full bg-no-repeat bg-center bg-blend-difference bg-contain bg-ch-gray-800"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -133,7 +140,7 @@ export const Success = ({
|
||||
CadHub
|
||||
</h2>
|
||||
<div
|
||||
className="text-pink-400 text-sm font-bold font-ropa-sans hidden md:block"
|
||||
className="text-pink-400 text-sm font-bold font-ropa-sans hidden md:block whitespace-nowrap"
|
||||
style={{ paddingBottom: '2rem', marginLeft: '-1.8rem' }}
|
||||
>
|
||||
pre-alpha
|
||||
|
||||
@@ -33,3 +33,16 @@ export const canvasToBlob = async (
|
||||
updateCanvasSize(oldSize)
|
||||
return imgBlobPromise
|
||||
}
|
||||
|
||||
export const blobTo64 = async (blob: Blob): Promise<string> => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onloadend = () => {
|
||||
if (typeof reader.result === 'string') {
|
||||
resolve(reader.result)
|
||||
}
|
||||
}
|
||||
reader.onerror = reject
|
||||
reader.readAsDataURL(blob)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
// TODO: create a tidy util for uploading to Cloudinary and returning the public ID
|
||||
import axios from 'axios'
|
||||
|
||||
const CLOUDINARY_UPLOAD_PRESET = 'CadHub_project_images'
|
||||
const CLOUDINARY_UPLOAD_URL = 'https://api.cloudinary.com/v1_1/irevdev/upload'
|
||||
|
||||
export async function uploadToCloudinary(imgBlob) {
|
||||
const imageData = new FormData()
|
||||
imageData.append('upload_preset', CLOUDINARY_UPLOAD_PRESET)
|
||||
imageData.append('file', imgBlob)
|
||||
let upload = axios.post(CLOUDINARY_UPLOAD_URL, imageData)
|
||||
|
||||
try {
|
||||
const { data } = await upload
|
||||
if (data && data.public_id !== '') {
|
||||
return data
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('ERROR', e)
|
||||
}
|
||||
}
|
||||
57
app/web/src/helpers/cloudinary.ts
Normal file
57
app/web/src/helpers/cloudinary.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
// TODO: create a tidy util for uploading to Cloudinary and returning the public ID
|
||||
import axios from 'axios'
|
||||
|
||||
const CLOUDINARY_UPLOAD_PRESET = 'CadHub_project_images'
|
||||
const CLOUDINARY_UPLOAD_URL = 'https://api.cloudinary.com/v1_1/irevdev/upload'
|
||||
|
||||
export async function uploadToCloudinary(
|
||||
imgBlob: Blob,
|
||||
publicId?: string
|
||||
): Promise<{ public_id: string }> {
|
||||
const imageData = new FormData()
|
||||
imageData.append('upload_preset', CLOUDINARY_UPLOAD_PRESET)
|
||||
imageData.append('file', imgBlob)
|
||||
if (publicId) {
|
||||
imageData.append('public_id', publicId)
|
||||
}
|
||||
const upload = axios.post(CLOUDINARY_UPLOAD_URL, imageData)
|
||||
|
||||
try {
|
||||
const { data } = await upload
|
||||
if (data && data.public_id !== '') {
|
||||
return data
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('ERROR', e)
|
||||
}
|
||||
}
|
||||
|
||||
export async function serverVerifiedImageUpload(
|
||||
imgBlob: string,
|
||||
projectId: string,
|
||||
token: string,
|
||||
publicId?: string
|
||||
): Promise<{ publicId: string }> {
|
||||
const imageData = {
|
||||
image64: imgBlob,
|
||||
upload_preset: CLOUDINARY_UPLOAD_PRESET,
|
||||
public_id: publicId,
|
||||
invalidate: true,
|
||||
projectId,
|
||||
}
|
||||
const upload = axios.post('/.netlify/functions/image-upload', imageData, {
|
||||
headers: {
|
||||
'auth-provider': 'goTrue',
|
||||
authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
|
||||
try {
|
||||
const { data } = await upload
|
||||
if (data && data.public_id !== '') {
|
||||
return data
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('ERROR', e)
|
||||
}
|
||||
}
|
||||
24
app/web/src/helpers/hooks/useUpdateSocialCard.ts
Normal file
24
app/web/src/helpers/hooks/useUpdateSocialCard.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { useMutation } from '@redwoodjs/web'
|
||||
|
||||
const UPDATE_SOCIAL_CARD_MUTATION_HOOK = gql`
|
||||
mutation updateSocialCardByProjectId($projectId: String!, $url: String!) {
|
||||
updateSocialCardByProjectId(projectId: $projectId, url: $url) {
|
||||
id
|
||||
url
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const useUpdateSocialCard = ({ onCompleted = () => {} }) => {
|
||||
const [updateSocialCard, { loading, error }] = useMutation(
|
||||
UPDATE_SOCIAL_CARD_MUTATION_HOOK,
|
||||
{ onCompleted }
|
||||
)
|
||||
|
||||
return { updateSocialCard, loading, error }
|
||||
}
|
||||
|
||||
export const makeSocialPublicId = (
|
||||
userName: string,
|
||||
projectTitle: string
|
||||
): string => `u-${userName}-slash-p-${projectTitle}`
|
||||
@@ -5,16 +5,15 @@ import Seo from 'src/components/Seo/Seo'
|
||||
import { useIdeState } from 'src/helpers/hooks/useIdeState'
|
||||
import { IdeContext } from 'src/helpers/hooks/useIdeContext'
|
||||
import { Toaster } from '@redwoodjs/web/toast'
|
||||
import { makeSocialPublicId } from 'src/helpers/hooks/useUpdateSocialCard'
|
||||
|
||||
const ProjectPage = ({ userName, projectTitle }) => {
|
||||
const { currentUser } = useAuth()
|
||||
const [state, thunkDispatch] = useIdeState()
|
||||
const cacheInvalidator = new Date()
|
||||
.toISOString()
|
||||
.split('-')
|
||||
.slice(0, 2)
|
||||
.join('-')
|
||||
const socialImageUrl = `/.netlify/functions/og-image-generator/${userName}/${projectTitle}/og-image-${cacheInvalidator}.jpg`
|
||||
const socialImageUrl = `http://res.cloudinary.com/irevdev/image/upload/c_scale,w_1200/v1/CadHub/${makeSocialPublicId(
|
||||
userName,
|
||||
projectTitle
|
||||
)}`
|
||||
return (
|
||||
<>
|
||||
<Seo
|
||||
|
||||
Reference in New Issue
Block a user