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:
@@ -0,0 +1,17 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "SocialCard" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"projectId" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"url" TEXT,
|
||||||
|
"outOfDate" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "SocialCard_projectId_unique" ON "SocialCard"("projectId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "SocialCard" ADD FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -56,12 +56,24 @@ model Project {
|
|||||||
userId String
|
userId String
|
||||||
deleted Boolean @default(false)
|
deleted Boolean @default(false)
|
||||||
cadPackage CadPackage @default(openscad)
|
cadPackage CadPackage @default(openscad)
|
||||||
|
socialCard SocialCard?
|
||||||
|
|
||||||
Comment Comment[]
|
Comment Comment[]
|
||||||
Reaction ProjectReaction[]
|
Reaction ProjectReaction[]
|
||||||
@@unique([title, userId])
|
@@unique([title, userId])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model SocialCard {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
projectId String
|
||||||
|
project Project @relation(fields: [projectId], references: [id])
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
url String? // link to cloudinary
|
||||||
|
outOfDate Boolean @default(true)
|
||||||
|
}
|
||||||
|
|
||||||
model ProjectReaction {
|
model ProjectReaction {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
emote String // an emoji
|
emote String // an emoji
|
||||||
|
|||||||
72
app/api/src/functions/image-upload.ts
Normal file
72
app/api/src/functions/image-upload.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import type { APIGatewayEvent } from 'aws-lambda'
|
||||||
|
import { logger } from 'src/lib/logger'
|
||||||
|
import { v2 as cloudinary, UploadApiResponse } from 'cloudinary'
|
||||||
|
// import { requireOwnership } from 'src/lib/owner'
|
||||||
|
// import { requireAuth } from 'src/lib/auth'
|
||||||
|
|
||||||
|
cloudinary.config({
|
||||||
|
cloud_name: 'irevdev',
|
||||||
|
api_key: process.env.CLOUDINARY_API_KEY,
|
||||||
|
api_secret: process.env.CLOUDINARY_API_SECRET,
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The handler function is your code that processes http request events.
|
||||||
|
* You can use return and throw to send a response or error, respectively.
|
||||||
|
*
|
||||||
|
* Important: When deployed, a custom serverless function is an open API endpoint and
|
||||||
|
* is your responsibility to secure appropriately.
|
||||||
|
*
|
||||||
|
* @see {@link https://redwoodjs.com/docs/serverless-functions#security-considerations|Serverless Function Considerations}
|
||||||
|
* in the RedwoodJS documentation for more information.
|
||||||
|
*
|
||||||
|
* @typedef { import('aws-lambda').APIGatewayEvent } APIGatewayEvent
|
||||||
|
* @typedef { import('aws-lambda').Context } Context
|
||||||
|
* @param { APIGatewayEvent } event - an object which contains information from the invoker.
|
||||||
|
* @param { Context } context - contains information about the invocation,
|
||||||
|
* function, and execution environment.
|
||||||
|
*/
|
||||||
|
export const handler = async (event: APIGatewayEvent, _context) => {
|
||||||
|
logger.info('Invoked image-upload function')
|
||||||
|
// requireAuth()
|
||||||
|
const {
|
||||||
|
image64,
|
||||||
|
upload_preset,
|
||||||
|
public_id,
|
||||||
|
invalidate,
|
||||||
|
projectId,
|
||||||
|
}: {
|
||||||
|
image64: string
|
||||||
|
upload_preset: string
|
||||||
|
public_id: string
|
||||||
|
invalidate: boolean
|
||||||
|
projectId: string
|
||||||
|
} = JSON.parse(event.body)
|
||||||
|
// await requireOwnership({projectId})
|
||||||
|
|
||||||
|
const uploadResult: UploadApiResponse = await new Promise(
|
||||||
|
(resolve, reject) => {
|
||||||
|
cloudinary.uploader.upload(
|
||||||
|
image64,
|
||||||
|
{ upload_preset, public_id, invalidate },
|
||||||
|
(error, result) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resolve(result)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
publicId: uploadResult.public_id,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ export const schema = gql`
|
|||||||
userId: String!
|
userId: String!
|
||||||
deleted: Boolean!
|
deleted: Boolean!
|
||||||
cadPackage: CadPackage!
|
cadPackage: CadPackage!
|
||||||
|
socialCard: SocialCard
|
||||||
Comment: [Comment]!
|
Comment: [Comment]!
|
||||||
Reaction(userId: String): [ProjectReaction]!
|
Reaction(userId: String): [ProjectReaction]!
|
||||||
}
|
}
|
||||||
|
|||||||
35
app/api/src/graphql/socialCards.sdl.ts
Normal file
35
app/api/src/graphql/socialCards.sdl.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
export const schema = gql`
|
||||||
|
type SocialCard {
|
||||||
|
id: String!
|
||||||
|
projectId: String!
|
||||||
|
project: Project!
|
||||||
|
createdAt: DateTime!
|
||||||
|
updatedAt: DateTime!
|
||||||
|
url: String
|
||||||
|
outOfDate: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
socialCards: [SocialCard!]!
|
||||||
|
socialCard(id: String!): SocialCard
|
||||||
|
}
|
||||||
|
|
||||||
|
input CreateSocialCardInput {
|
||||||
|
projectId: String!
|
||||||
|
url: String
|
||||||
|
outOfDate: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
|
input UpdateSocialCardInput {
|
||||||
|
projectId: String
|
||||||
|
url: String
|
||||||
|
outOfDate: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
createSocialCard(input: CreateSocialCardInput!): SocialCard!
|
||||||
|
updateSocialCard(id: String!, input: UpdateSocialCardInput!): SocialCard!
|
||||||
|
deleteSocialCard(id: String!): SocialCard!
|
||||||
|
updateSocialCardByProjectId(projectId: String!, url: String!): SocialCard!
|
||||||
|
}
|
||||||
|
`
|
||||||
@@ -5,10 +5,16 @@ export const requireOwnership = async ({
|
|||||||
userId,
|
userId,
|
||||||
userName,
|
userName,
|
||||||
projectId,
|
projectId,
|
||||||
}: { userId?: string; userName?: string; projectId?: string } = {}) => {
|
sub,
|
||||||
|
}: {
|
||||||
|
userId?: string
|
||||||
|
userName?: string
|
||||||
|
projectId?: string
|
||||||
|
sub?: string
|
||||||
|
} = {}) => {
|
||||||
// IMPORTANT, don't forget to await this function, as it will only block
|
// IMPORTANT, don't forget to await this function, as it will only block
|
||||||
// unwanted db actions if it has time to look up resources in the db.
|
// unwanted db actions if it has time to look up resources in the db.
|
||||||
if (!context.currentUser) {
|
if (!(context?.currentUser || sub)) {
|
||||||
throw new AuthenticationError("You don't have permission to do that.")
|
throw new AuthenticationError("You don't have permission to do that.")
|
||||||
}
|
}
|
||||||
if (!userId && !userName && !projectId) {
|
if (!userId && !userName && !projectId) {
|
||||||
@@ -22,7 +28,7 @@ export const requireOwnership = async ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const netlifyUserId = context.currentUser?.sub
|
const netlifyUserId = context?.currentUser?.sub || sub
|
||||||
if (userId && userId !== netlifyUserId) {
|
if (userId && userId !== netlifyUserId) {
|
||||||
throw new ForbiddenError("You don't own this resource.")
|
throw new ForbiddenError("You don't own this resource.")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,6 +130,8 @@ export const deleteProject = async ({ id }: Prisma.ProjectWhereUniqueInput) => {
|
|||||||
export const Project = {
|
export const Project = {
|
||||||
user: (_obj, { root }: ResolverArgs<ReturnType<typeof project>>) =>
|
user: (_obj, { root }: ResolverArgs<ReturnType<typeof project>>) =>
|
||||||
db.project.findUnique({ where: { id: root.id } }).user(),
|
db.project.findUnique({ where: { id: root.id } }).user(),
|
||||||
|
socialCard: (_obj, { root }: ResolverArgs<ReturnType<typeof project>>) =>
|
||||||
|
db.project.findUnique({ where: { id: root.id } }).socialCard(),
|
||||||
Comment: (_obj, { root }: ResolverArgs<ReturnType<typeof project>>) =>
|
Comment: (_obj, { root }: ResolverArgs<ReturnType<typeof project>>) =>
|
||||||
db.project
|
db.project
|
||||||
.findUnique({ where: { id: root.id } })
|
.findUnique({ where: { id: root.id } })
|
||||||
|
|||||||
84
app/api/src/services/socialCards/socialCards.ts
Normal file
84
app/api/src/services/socialCards/socialCards.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import type { Prisma } from '@prisma/client'
|
||||||
|
import type { ResolverArgs, BeforeResolverSpecType } from '@redwoodjs/api'
|
||||||
|
|
||||||
|
import { db } from 'src/lib/db'
|
||||||
|
import { requireAuth } from 'src/lib/auth'
|
||||||
|
|
||||||
|
// Used when the environment variable REDWOOD_SECURE_SERVICES=1
|
||||||
|
export const beforeResolver = (rules: BeforeResolverSpecType) => {
|
||||||
|
rules.add(requireAuth)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const socialCards = () => {
|
||||||
|
return db.socialCard.findMany()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const socialCard = ({ id }: Prisma.SocialCardWhereUniqueInput) => {
|
||||||
|
return db.socialCard.findUnique({
|
||||||
|
where: { id },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CreateSocialCardArgs {
|
||||||
|
input: Prisma.SocialCardCreateInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createSocialCard = ({ input }: CreateSocialCardArgs) => {
|
||||||
|
return db.socialCard.create({
|
||||||
|
data: input,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateSocialCardArgs extends Prisma.SocialCardWhereUniqueInput {
|
||||||
|
input: Prisma.SocialCardUpdateInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateSocialCard = ({ id, input }: UpdateSocialCardArgs) => {
|
||||||
|
return db.socialCard.update({
|
||||||
|
data: input,
|
||||||
|
where: { id },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateSocialCardByProjectId = async ({
|
||||||
|
projectId,
|
||||||
|
url,
|
||||||
|
}: {
|
||||||
|
url: string
|
||||||
|
projectId: string
|
||||||
|
}) => {
|
||||||
|
let id: string
|
||||||
|
try {
|
||||||
|
const socialCard = await db.project
|
||||||
|
.findUnique({ where: { id: projectId } })
|
||||||
|
.socialCard()
|
||||||
|
id = socialCard.id
|
||||||
|
} catch (e) {
|
||||||
|
return db.socialCard.create({
|
||||||
|
data: {
|
||||||
|
url,
|
||||||
|
project: {
|
||||||
|
connect: {
|
||||||
|
id: projectId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.socialCard.update({
|
||||||
|
data: { url },
|
||||||
|
where: { id },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteSocialCard = ({ id }: Prisma.SocialCardWhereUniqueInput) => {
|
||||||
|
return db.socialCard.delete({
|
||||||
|
where: { id },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SocialCard = {
|
||||||
|
project: (_obj, { root }: ResolverArgs<ReturnType<typeof socialCard>>) =>
|
||||||
|
db.socialCard.findUnique({ where: { id: root.id } }).project(),
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
"cloudinary-react": "^1.6.7",
|
"cloudinary-react": "^1.6.7",
|
||||||
"get-active-classes": "^0.0.11",
|
"get-active-classes": "^0.0.11",
|
||||||
"gotrue-js": "^0.9.27",
|
"gotrue-js": "^0.9.27",
|
||||||
|
"html-to-image": "^1.7.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"netlify-identity-widget": "^1.9.1",
|
"netlify-identity-widget": "^1.9.1",
|
||||||
"pako": "^2.0.3",
|
"pako": "^2.0.3",
|
||||||
|
|||||||
@@ -4,8 +4,19 @@ import Popover from '@material-ui/core/Popover'
|
|||||||
import Svg from 'src/components/Svg/Svg'
|
import Svg from 'src/components/Svg/Svg'
|
||||||
import Button from 'src/components/Button/Button'
|
import Button from 'src/components/Button/Button'
|
||||||
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
|
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
|
||||||
|
import { canvasToBlob, blobTo64 } from 'src/helpers/canvasToBlob'
|
||||||
import { useUpdateProject } from 'src/helpers/hooks/useUpdateProject'
|
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 = {
|
const anchorOrigin = {
|
||||||
vertical: 'bottom',
|
vertical: 'bottom',
|
||||||
@@ -16,51 +27,50 @@ const transformOrigin = {
|
|||||||
horizontal: 'center',
|
horizontal: 'center',
|
||||||
}
|
}
|
||||||
|
|
||||||
const CaptureButton = ({ canEdit, TheButton, shouldUpdateImage }) => {
|
const CaptureButton = ({
|
||||||
|
canEdit,
|
||||||
|
TheButton,
|
||||||
|
shouldUpdateImage,
|
||||||
|
projectTitle,
|
||||||
|
userName,
|
||||||
|
}) => {
|
||||||
const [captureState, setCaptureState] = useState<any>({})
|
const [captureState, setCaptureState] = useState<any>({})
|
||||||
const [anchorEl, setAnchorEl] = useState(null)
|
const [anchorEl, setAnchorEl] = useState(null)
|
||||||
const [whichPopup, setWhichPopup] = useState(null)
|
const [whichPopup, setWhichPopup] = useState(null)
|
||||||
const { state, project } = useIdeContext()
|
const { state, project } = useIdeContext()
|
||||||
|
const ref = React.useRef<HTMLDivElement>(null)
|
||||||
const { updateProject } = useUpdateProject({
|
const { updateProject } = useUpdateProject({
|
||||||
onCompleted: () => toast.success('Image updated'),
|
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 onCapture = async () => {
|
||||||
const threeInstance = state.threeInstance
|
const threeInstance = state.threeInstance
|
||||||
const isOpenScadImage = state?.objectData?.type === 'png'
|
const isOpenScadImage = state?.objectData?.type === 'png'
|
||||||
let imgBlob
|
let imgBlob
|
||||||
|
let image64
|
||||||
if (!isOpenScadImage) {
|
if (!isOpenScadImage) {
|
||||||
const updateCanvasSize = ({
|
imgBlob = canvasToBlob(threeInstance, { width: 400, height: 300 })
|
||||||
width,
|
image64 = blobTo64(
|
||||||
height,
|
await canvasToBlob(threeInstance, { width: 500, height: 522 })
|
||||||
}: {
|
)
|
||||||
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)
|
|
||||||
} else {
|
} else {
|
||||||
imgBlob = state.objectData.data
|
imgBlob = state.objectData.data
|
||||||
|
image64 = blobTo64(state.objectData.data)
|
||||||
}
|
}
|
||||||
const config = {
|
const config = {
|
||||||
image: await imgBlob,
|
image: await imgBlob,
|
||||||
@@ -69,11 +79,22 @@ const CaptureButton = ({ canEdit, TheButton, shouldUpdateImage }) => {
|
|||||||
callback: uploadAndUpdateImage,
|
callback: uploadAndUpdateImage,
|
||||||
cloudinaryImgURL: '',
|
cloudinaryImgURL: '',
|
||||||
updated: false,
|
updated: false,
|
||||||
|
image64: await image64,
|
||||||
}
|
}
|
||||||
|
setCaptureState(config)
|
||||||
|
|
||||||
async function uploadAndUpdateImage() {
|
async function uploadAndUpdateImage() {
|
||||||
// Upload the image to Cloudinary
|
const [cloudinaryImgURL, socialCloudinaryURL] = await Promise.all([
|
||||||
const cloudinaryImgURL = await uploadToCloudinary(config.image)
|
uploadToCloudinary(config.image),
|
||||||
|
getSocialBlob(),
|
||||||
|
])
|
||||||
|
|
||||||
|
updateSocialCard({
|
||||||
|
variables: {
|
||||||
|
projectId: project?.id,
|
||||||
|
url: socialCloudinaryURL,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
// Save the screenshot as the mainImage
|
// Save the screenshot as the mainImage
|
||||||
updateProject({
|
updateProject({
|
||||||
@@ -92,9 +113,8 @@ const CaptureButton = ({ canEdit, TheButton, shouldUpdateImage }) => {
|
|||||||
if (shouldUpdateImage) {
|
if (shouldUpdateImage) {
|
||||||
config.cloudinaryImgURL = (await uploadAndUpdateImage()).public_id
|
config.cloudinaryImgURL = (await uploadAndUpdateImage()).public_id
|
||||||
config.updated = true
|
config.updated = true
|
||||||
|
setCaptureState(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDownload = (url) => {
|
const handleDownload = (url) => {
|
||||||
@@ -123,7 +143,7 @@ const CaptureButton = ({ canEdit, TheButton, shouldUpdateImage }) => {
|
|||||||
<TheButton
|
<TheButton
|
||||||
onClick={async (event) => {
|
onClick={async (event) => {
|
||||||
handleClick({ event, whichPopup: 'capture' })
|
handleClick({ event, whichPopup: 'capture' })
|
||||||
setCaptureState(await onCapture())
|
onCapture()
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Popover
|
<Popover
|
||||||
@@ -163,7 +183,7 @@ const CaptureButton = ({ canEdit, TheButton, shouldUpdateImage }) => {
|
|||||||
name="refresh"
|
name="refresh"
|
||||||
className="mr-2 w-4 text-indigo-600"
|
className="mr-2 w-4 text-indigo-600"
|
||||||
/>{' '}
|
/>{' '}
|
||||||
Update Part Image
|
Update Project Image
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex justify-center mb-4">
|
<div className="flex justify-center mb-4">
|
||||||
@@ -187,6 +207,20 @@ const CaptureButton = ({ canEdit, TheButton, shouldUpdateImage }) => {
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,12 +37,13 @@ export const Failure = ({ error }: CellFailureProps) => (
|
|||||||
|
|
||||||
export const Success = ({
|
export const Success = ({
|
||||||
userProject,
|
userProject,
|
||||||
|
variables: { image64 },
|
||||||
}: CellSuccessProps<FindSocialCardQuery>) => {
|
}: CellSuccessProps<FindSocialCardQuery>) => {
|
||||||
const image = userProject?.Project?.mainImage
|
const image = userProject?.Project?.mainImage
|
||||||
const gravatar = userProject?.image
|
const gravatar = userProject?.image
|
||||||
return (
|
return (
|
||||||
<div
|
<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"
|
id="social-card-loaded"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -65,7 +66,7 @@ export const Success = ({
|
|||||||
{gravatar && (
|
{gravatar && (
|
||||||
<Gravatar image={gravatar} className="w-14 h-14" size={60} />
|
<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}
|
{userProject?.userName}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -86,13 +87,19 @@ export const Success = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="h-full overflow-hidden relative">
|
<div className="h-full overflow-hidden relative">
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
<CloudinaryImage
|
{image64 ? (
|
||||||
cloudName="irevdev"
|
<div
|
||||||
publicId={image || 'CadHub/eia1kwru54g2kf02s2xx'}
|
style={{ backgroundImage: `url(${image64})` }}
|
||||||
width={500}
|
className="w-full h-full bg-no-repeat bg-center"
|
||||||
height={522}
|
/>
|
||||||
crop="crop"
|
) : (
|
||||||
/>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -133,7 +140,7 @@ export const Success = ({
|
|||||||
CadHub
|
CadHub
|
||||||
</h2>
|
</h2>
|
||||||
<div
|
<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' }}
|
style={{ paddingBottom: '2rem', marginLeft: '-1.8rem' }}
|
||||||
>
|
>
|
||||||
pre-alpha
|
pre-alpha
|
||||||
|
|||||||
@@ -33,3 +33,16 @@ export const canvasToBlob = async (
|
|||||||
updateCanvasSize(oldSize)
|
updateCanvasSize(oldSize)
|
||||||
return imgBlobPromise
|
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 { useIdeState } from 'src/helpers/hooks/useIdeState'
|
||||||
import { IdeContext } from 'src/helpers/hooks/useIdeContext'
|
import { IdeContext } from 'src/helpers/hooks/useIdeContext'
|
||||||
import { Toaster } from '@redwoodjs/web/toast'
|
import { Toaster } from '@redwoodjs/web/toast'
|
||||||
|
import { makeSocialPublicId } from 'src/helpers/hooks/useUpdateSocialCard'
|
||||||
|
|
||||||
const ProjectPage = ({ userName, projectTitle }) => {
|
const ProjectPage = ({ userName, projectTitle }) => {
|
||||||
const { currentUser } = useAuth()
|
const { currentUser } = useAuth()
|
||||||
const [state, thunkDispatch] = useIdeState()
|
const [state, thunkDispatch] = useIdeState()
|
||||||
const cacheInvalidator = new Date()
|
const socialImageUrl = `http://res.cloudinary.com/irevdev/image/upload/c_scale,w_1200/v1/CadHub/${makeSocialPublicId(
|
||||||
.toISOString()
|
userName,
|
||||||
.split('-')
|
projectTitle
|
||||||
.slice(0, 2)
|
)}`
|
||||||
.join('-')
|
|
||||||
const socialImageUrl = `/.netlify/functions/og-image-generator/${userName}/${projectTitle}/og-image-${cacheInvalidator}.jpg`
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Seo
|
<Seo
|
||||||
|
|||||||
@@ -10198,6 +10198,11 @@ html-tags@^3.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.1.0.tgz#7b5e6f7e665e9fb41f30007ed9e0d41e97fb2140"
|
resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.1.0.tgz#7b5e6f7e665e9fb41f30007ed9e0d41e97fb2140"
|
||||||
integrity sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==
|
integrity sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==
|
||||||
|
|
||||||
|
html-to-image@^1.7.0:
|
||||||
|
version "1.7.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/html-to-image/-/html-to-image-1.7.0.tgz#4ca93bb90c0b9392edaafbfd5d94e8f0d666e18b"
|
||||||
|
integrity sha512-6egK8mOXMw82nLjj5g3ohERuzrTglgR9+Q6A2cqa7UiuSSKHuFxpABZJSfZztj0EdLC6tAePZJAhjPr4bbU9tw==
|
||||||
|
|
||||||
html-webpack-plugin@4.0.2:
|
html-webpack-plugin@4.0.2:
|
||||||
version "4.0.2"
|
version "4.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-4.0.2.tgz#c96a48d0ee53d33dcc909d6b65ad28f3d627efd4"
|
resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-4.0.2.tgz#c96a48d0ee53d33dcc909d6b65ad28f3d627efd4"
|
||||||
|
|||||||
Reference in New Issue
Block a user