Merge pull request #458 from Irev-Dev/kurt/97-clean-up

social card clean up
This commit was merged in pull request #458.
This commit is contained in:
Kurt Hutten
2021-08-17 05:10:26 +10:00
committed by GitHub
19 changed files with 364 additions and 383 deletions

View File

@@ -5,18 +5,10 @@ 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 {
useUpdateSocialCard,
makeSocialPublicId,
} from 'src/helpers/hooks/useUpdateSocialCard'
import {
uploadToCloudinary,
serverVerifiedImageUpload,
} from 'src/helpers/cloudinary'
import { useUpdateProjectImages } from 'src/helpers/hooks/useUpdateProjectImages'
import SocialCardCell from 'src/components/SocialCardCell/SocialCardCell'
import { toJpeg } from 'html-to-image'
import { useAuth } from '@redwoodjs/auth'
const anchorOrigin = {
vertical: 'bottom',
@@ -39,24 +31,7 @@ const CaptureButton = ({
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 { updateProjectImages } = useUpdateProjectImages({})
const onCapture = async () => {
const threeInstance = state.threeInstance
@@ -64,7 +39,7 @@ const CaptureButton = ({
let imgBlob
let image64
if (!isOpenScadImage) {
imgBlob = canvasToBlob(threeInstance, { width: 400, height: 300 })
imgBlob = canvasToBlob(threeInstance, { width: 500, height: 375 })
image64 = blobTo64(
await canvasToBlob(threeInstance, { width: 500, height: 522 })
)
@@ -84,49 +59,45 @@ const CaptureButton = ({
setCaptureState(config)
async function uploadAndUpdateImage() {
const [cloudinaryImgURL, socialCloudinaryURL] = await Promise.all([
uploadToCloudinary(config.image),
getSocialBlob(),
])
const upload = async () => {
const socialCard64 = toJpeg(ref.current, {
cacheBust: true,
quality: 0.7,
})
updateSocialCard({
variables: {
projectId: project?.id,
url: socialCloudinaryURL,
},
})
// Save the screenshot as the mainImage
updateProject({
variables: {
id: project?.id,
input: {
mainImage: cloudinaryImgURL.public_id,
// uploading in two separate mutations because of the 100kb limit of the lambda functions
const imageUploadPromise1 = updateProjectImages({
variables: {
id: project?.id,
mainImage64: await config.image64,
},
},
})
const imageUploadPromise2 = updateProjectImages({
variables: {
id: project?.id,
socialCard64: await socialCard64,
},
})
return Promise.all([imageUploadPromise2, imageUploadPromise1])
}
const promise = upload()
toast.promise(promise, {
loading: 'Saving Image/s',
success: <b>Image/s saved!</b>,
error: <b>Problem saving.</b>,
})
return cloudinaryImgURL
const [{ data }] = await promise
return data?.updateProjectImages?.mainImage
}
// if there isn't a screenshot saved yet, just go ahead and save right away
if (shouldUpdateImage) {
config.cloudinaryImgURL = (await uploadAndUpdateImage()).public_id
config.cloudinaryImgURL = await uploadAndUpdateImage()
config.updated = true
setCaptureState(config)
}
}
const handleDownload = (url) => {
const aTag = document.createElement('a')
document.body.appendChild(aTag)
aTag.href = url
aTag.style.display = 'none'
aTag.download = `${project?.title}-${new Date().toISOString()}.jpg`
aTag.click()
document.body.removeChild(aTag)
}
const handleClick = ({ event, whichPopup }) => {
setAnchorEl(event.currentTarget)
setWhichPopup(whichPopup)
@@ -155,59 +126,22 @@ const CaptureButton = ({
transformOrigin={transformOrigin}
className="material-ui-overrides transform translate-y-4"
>
<div className="text-sm p-2 text-gray-500">
<div className="text-sm p-4 text-gray-500">
{!captureState ? (
'Loading...'
) : (
<div className="grid grid-cols-2">
<div className="">
<div className="text-lg">Thumbnail</div>
<div
className="rounded m-auto"
className="rounded"
style={{ width: 'fit-content', overflow: 'hidden' }}
>
<img src={captureState.imageObjectURL} className="w-32" />
</div>
<div className="p-2 text-indigo-800">
{captureState.currImage && !captureState.updated ? (
<button
className="flex justify-center mb-4"
onClick={async () => {
const cloudinaryImg = await captureState.callback()
setCaptureState({
...captureState,
currImage: cloudinaryImg.public_id,
updated: true,
})
}}
>
<Svg
name="refresh"
className="mr-2 w-4 text-indigo-600"
/>{' '}
Update Project Image
</button>
) : (
<div className="flex justify-center mb-4">
<Svg
name="checkmark"
className="mr-2 w-6 text-indigo-600"
/>{' '}
Part Image Updated
</div>
)}
<Button
iconName="save"
className="shadow-md hover:shadow-lg border-indigo-600 border-2 border-opacity-0 hover:border-opacity-100 bg-indigo-200 text-indigo-100 text-opacity-100 bg-opacity-80"
shouldAnimateHover
onClick={() =>
handleDownload(captureState.imageObjectURL)
}
>
Download
</Button>
</div>
</div>
)}
<div className="rounded-lg shadow-md mt-4 overflow-hidden">
<div className="text-lg mt-4">Social Media Card</div>
<div className="rounded-lg shadow-md overflow-hidden">
<div
className="transform scale-50 origin-top-left"
style={{ width: '600px', height: '315px' }}
@@ -221,6 +155,33 @@ const CaptureButton = ({
</div>
</div>
</div>
<div className="mt-4 text-indigo-800">
{captureState.currImage && !captureState.updated ? (
<Button
iconName="refresh"
className="shadow-md hover:shadow-lg border-indigo-600 border-2 border-opacity-0 hover:border-opacity-100 bg-indigo-200 text-indigo-100 text-opacity-100 bg-opacity-80"
shouldAnimateHover
onClick={async () => {
const cloudinaryImg = await captureState.callback()
setCaptureState({
...captureState,
currImage: cloudinaryImg,
updated: true,
})
}}
>
Update Project Images
</Button>
) : (
<div className="flex justify-center mb-4">
<Svg
name="checkmark"
className="mr-2 w-6 text-indigo-600"
/>{' '}
Project Images Updated
</div>
)}
</div>
</div>
</Popover>
</div>

View File

@@ -2,8 +2,8 @@ import { Helmet } from 'react-helmet'
import { useIsBrowser } from '@redwoodjs/prerender/browserUtils'
const Seo = ({
title = "CadHub",
description = "Edit this part of CadHub",
title = 'CadHub',
description = 'Edit this part of CadHub',
lang = 'en-US',
socialImageUrl,
}: {
@@ -22,25 +22,33 @@ const Seo = ({
title={title}
titleTemplate={`Cadhub - ${title}`}
>
<title>{title || 'cadhub'}</title>
<meta name="description" content={description} />
{title && <title>{title || 'cadhub'}</title>}
{description && <meta name="description" content={description} />}
{/* Facebook Meta Tags */}
{browser && <meta property="og:url" content={location.href} />}
<meta property="og:type" content="website" />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={socialImageUrl} />
{title && <meta property="og:title" content={title} />}
{description && (
<meta property="og:description" content={description} />
)}
{socialImageUrl && (
<meta property="og:image" content={socialImageUrl} />
)}
{/* Twitter Meta Tags */}
<meta name="twitter:card" content="summary_large_image" />
<meta property="twitter:domain" content="cadhub.xyz" />
{browser && <meta property="twitter:url" content={location.href} />}
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={socialImageUrl} />
{title && <meta name="twitter:title" content={title} />}
{description && (
<meta name="twitter:description" content={description} />
)}
{socialImageUrl && (
<meta name="twitter:image" content={socialImageUrl} />
)}
<meta property="og:locale" content={lang} />
{lang && <meta property="og:locale" content={lang} />}
</Helmet>
</>
)

View File

@@ -41,6 +41,10 @@ export const Success = ({
}: CellSuccessProps<FindSocialCardQuery>) => {
const image = userProject?.Project?.mainImage
const gravatar = userProject?.image
const truncatedDescription =
userProject?.Project?.description?.length > 150
? (userProject?.Project?.description || '').slice(0, 145) + ' . . .'
: userProject?.Project?.description || ''
return (
<div
className="flex-col flex h-full bg-ch-gray-800 text-ch-gray-300"
@@ -51,37 +55,30 @@ export const Success = ({
style={{ gridTemplateColumns: '7fr 5fr' }}
>
<div className="bg-ch-gray-800 relative">
<div className="absolute bottom-0 left-0 transform scale-200 aspect-h-1 h-full -translate-x-24 translate-y-24 rotate-45 rounded-full overflow-hidden">
{/* <CloudinaryImage
cloudName="irevdev"
publicId={image || 'CadHub/eia1kwru54g2kf02s2xx'}
width={500}
crop="scale"
/> */}
</div>
<div className="absolute bottom-0 left-0 transform scale-200 aspect-h-1 h-full -translate-x-24 translate-y-24 rotate-45 rounded-full overflow-hidden"></div>
<div className="relative bg-ch-gray-760 bg-opacity-90 pt-10 pl-20 pr-12 h-full backdrop-filter backdrop-blur">
<div className="flex justify-between items-center">
<div className="flex items-center">
{gravatar && (
<Gravatar image={gravatar} className="w-14 h-14" size={60} />
<Gravatar image={gravatar} className="w-18 h-18" size={60} />
)}
<div className="text-2xl font-fira-sans ml-6 whitespace-nowrap">
<div className="text-3xl font-fira-sans ml-6 whitespace-nowrap">
{userProject?.userName}
</div>
</div>
<CadPackage
cadPackage={userProject?.Project?.cadPackage}
className="p-2 rounded px-4"
className="p-2 rounded px-4 transform scale-150 origin-right"
/>
</div>
<h1 className="text-6xl font-fira-sans mt-16 capitalize">
<h1 className="text-6xl font-fira-sans mt-12 capitalize">
{userProject?.Project?.title.replace(/-/g, ' ')}
</h1>
<p className="mt-10 text-2xl font-fira-sans text-ch-gray-400">
{(userProject?.Project?.description || '').slice(0, 150)}
<p className="mt-10 text-3xl font-fira-sans text-ch-gray-400">
{truncatedDescription}
</p>
</div>
</div>
@@ -104,7 +101,7 @@ export const Success = ({
</div>
</div>
<div
className="h-24 grid bg-ch-gray-900 relative"
className="h-28 grid bg-ch-gray-900 relative"
style={{ gridTemplateColumns: '7fr 5fr' }}
>
<div className="grid grid-flow-col-dense items-center justify-center gap-16">
@@ -121,10 +118,10 @@ export const Success = ({
},
].map(({ svg, title, count }, index) => (
<div className="grid grid-flow-col-dense gap-4" key={index}>
<Svg className="w-10" name={svg} />
<Svg className="w-12" name={svg} />
<div className="flex flex-col">
<div className="text-3xl">{count}</div>
<div className="text-xl text-ch-gray-400">{title}</div>
<div className="text-4xl">{count}</div>
<div className="text-3xl text-ch-gray-400">{title}</div>
</div>
</div>
))}
@@ -134,17 +131,11 @@ export const Success = ({
<div className="ml-2 md:ml-6 flex">
{/* Because of how specific these styles are to this heading/logo and it doesn't need to be replicated else where as well as it's very precise with the placement of "pre-alpha" I think it's appropriate. */}
<h2
className="text-indigo-300 text-2xl md:text-5xl font-ropa-sans py-1 md:tracking-wider"
className="text-indigo-300 text-3xl md:text-5xl font-ropa-sans py-1 md:tracking-wider"
style={{ letterSpacing: '0.3em' }}
>
CadHub
</h2>
<div
className="text-pink-400 text-sm font-bold font-ropa-sans hidden md:block whitespace-nowrap"
style={{ paddingBottom: '2rem', marginLeft: '-1.8rem' }}
>
pre-alpha
</div>
</div>
</div>
</div>

View File

@@ -199,9 +199,13 @@ function getParams(target: HTMLElement): RawCustomizerParams {
if (
numeric[elem.getAttribute('type')] ||
elem.getAttribute('numeric') == '1'
){
) {
value = parseFloat(String(value || 0))
}else if (value && typeof(value) === 'string' && /^(\d+|\d+\.\d+)$/.test(value.trim())){
} else if (
value &&
typeof value === 'string' &&
/^(\d+|\d+\.\d+)$/.test(value.trim())
) {
value = parseFloat(String(value || 0))
}
if (elem.type == 'radio' && !elem.checked) return // skip if not checked radio button

View File

@@ -27,7 +27,7 @@ export const canvasToBlob = async (
resolve(blob)
},
'image/jpeg',
1
0.75
)
})
updateCanvasSize(oldSize)

View File

@@ -1,57 +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: 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)
}
}

View File

@@ -0,0 +1,36 @@
import { useMutation } from '@redwoodjs/web'
const UPDATE_PROJECT_IMAGES_MUTATION_HOOK = gql`
mutation updateProjectImages(
$id: String!
$mainImage64: String
$socialCard64: String
) {
updateProjectImages(
id: $id
mainImage64: $mainImage64
socialCard64: $socialCard64
) {
id
mainImage
socialCard {
id
url
}
}
}
`
export const useUpdateProjectImages = ({ onCompleted = () => {} }) => {
const [updateProjectImages, { loading, error }] = useMutation(
UPDATE_PROJECT_IMAGES_MUTATION_HOOK,
{ onCompleted }
)
return { updateProjectImages, loading, error }
}
export const makeSocialPublicId = (
userName: string,
projectTitle: string
): string => `u-${userName}-slash-p-${projectTitle}`

View File

@@ -1,24 +0,0 @@
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}`

View File

@@ -1,6 +1,6 @@
import IdeProjectCell from 'src/components/IdeProjectCell'
import Seo from 'src/components/Seo/Seo'
import { makeSocialPublicId } from 'src/helpers/hooks/useUpdateSocialCard'
import { makeSocialPublicId } from 'src/helpers/hooks/useUpdateProjectImages'
const IdeProjectPage = ({ userName, projectTitle }) => {
const socialImageUrl = `http://res.cloudinary.com/irevdev/image/upload/c_scale,w_1200/v1/CadHub/${makeSocialPublicId(
@@ -9,7 +9,12 @@ const IdeProjectPage = ({ userName, projectTitle }) => {
)}`
return (
<>
<Seo title={projectTitle} description={projectTitle} socialImageUrl={socialImageUrl} lang="en-US" />
<Seo
title={projectTitle}
description={projectTitle}
socialImageUrl={socialImageUrl}
lang="en-US"
/>
<IdeProjectCell userName={userName} projectTitle={projectTitle} />
</>
)

View File

@@ -5,7 +5,7 @@ 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'
import { makeSocialPublicId } from 'src/helpers/hooks/useUpdateProjectImages'
const ProjectPage = ({ userName, projectTitle }) => {
const { currentUser } = useAuth()