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:
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterIndex
|
||||||
|
ALTER INDEX "SocialCard_projectId_unique" RENAME TO "SocialCard.projectId_unique";
|
||||||
@@ -65,8 +65,8 @@ model Project {
|
|||||||
|
|
||||||
model SocialCard {
|
model SocialCard {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
projectId String
|
projectId String @unique
|
||||||
project Project @relation(fields: [projectId], references: [id])
|
project Project @relation(fields: [projectId], references: [id])
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
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,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -48,6 +48,11 @@ export const schema = gql`
|
|||||||
createProject(input: CreateProjectInput!): Project!
|
createProject(input: CreateProjectInput!): Project!
|
||||||
forkProject(input: CreateProjectInput!): Project!
|
forkProject(input: CreateProjectInput!): Project!
|
||||||
updateProject(id: String!, input: UpdateProjectInput!): Project!
|
updateProject(id: String!, input: UpdateProjectInput!): Project!
|
||||||
|
updateProjectImages(
|
||||||
|
id: String!
|
||||||
|
mainImage64: String
|
||||||
|
socialCard64: String
|
||||||
|
): Project!
|
||||||
deleteProject(id: String!): Project!
|
deleteProject(id: String!): Project!
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -13,23 +13,4 @@ export const schema = gql`
|
|||||||
socialCards: [SocialCard!]!
|
socialCards: [SocialCard!]!
|
||||||
socialCard(id: String!): 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!
|
|
||||||
}
|
|
||||||
`
|
`
|
||||||
|
|||||||
45
app/api/src/lib/cloudinary.ts
Normal file
45
app/api/src/lib/cloudinary.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import {
|
||||||
|
v2 as cloudinary,
|
||||||
|
UploadApiResponse,
|
||||||
|
UpdateApiOptions,
|
||||||
|
} from 'cloudinary'
|
||||||
|
|
||||||
|
cloudinary.config({
|
||||||
|
cloud_name: 'irevdev',
|
||||||
|
api_key: process.env.CLOUDINARY_API_KEY,
|
||||||
|
api_secret: process.env.CLOUDINARY_API_SECRET,
|
||||||
|
})
|
||||||
|
|
||||||
|
interface UploadImageArgs {
|
||||||
|
image64: string
|
||||||
|
uploadPreset?: string
|
||||||
|
publicId?: string
|
||||||
|
invalidate: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const uploadImage = async ({
|
||||||
|
image64,
|
||||||
|
uploadPreset = 'CadHub_project_images',
|
||||||
|
publicId,
|
||||||
|
invalidate = true,
|
||||||
|
}: UploadImageArgs): Promise<UploadApiResponse> => {
|
||||||
|
const options: UpdateApiOptions = { upload_preset: uploadPreset, invalidate }
|
||||||
|
if (publicId) {
|
||||||
|
options.public_id = publicId
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
cloudinary.uploader.upload(image64, options, (error, result) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resolve(result)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeSocialPublicIdServer = (
|
||||||
|
userName: string,
|
||||||
|
projectTitle: string
|
||||||
|
): string => `u-${userName}-slash-p-${projectTitle}`
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { AuthenticationError, ForbiddenError } from '@redwoodjs/api'
|
import { AuthenticationError, ForbiddenError } from '@redwoodjs/api'
|
||||||
|
import type { Project } from '@prisma/client'
|
||||||
import { db } from 'src/lib/db'
|
import { db } from 'src/lib/db'
|
||||||
|
|
||||||
export const requireOwnership = async ({
|
export const requireOwnership = async ({
|
||||||
@@ -55,3 +56,39 @@ export const requireOwnership = async ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export const requireProjectOwnership = async ({
|
||||||
|
projectId,
|
||||||
|
}: {
|
||||||
|
userId?: string
|
||||||
|
userName?: string
|
||||||
|
projectId?: string
|
||||||
|
sub?: string
|
||||||
|
} = {}): Promise<Project> => {
|
||||||
|
// 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.
|
||||||
|
if (!context?.currentUser) {
|
||||||
|
throw new AuthenticationError("You don't have permission to do that.")
|
||||||
|
}
|
||||||
|
if (!projectId) {
|
||||||
|
throw new ForbiddenError("You don't have access to do that.")
|
||||||
|
}
|
||||||
|
|
||||||
|
const netlifyUserId = context?.currentUser?.sub
|
||||||
|
|
||||||
|
if (projectId || context.currentUser.roles?.includes('admin')) {
|
||||||
|
if (context.currentUser?.sub === '5cea3906-1e8e-4673-8f0d-89e6a963c096') {
|
||||||
|
throw new ForbiddenError("That's a local admin ONLY.")
|
||||||
|
}
|
||||||
|
const project = await db.project.findUnique({
|
||||||
|
where: { id: projectId },
|
||||||
|
})
|
||||||
|
const hasPermission =
|
||||||
|
(project && project?.userId === netlifyUserId) ||
|
||||||
|
context.currentUser.roles?.includes('admin')
|
||||||
|
|
||||||
|
if (!hasPermission) {
|
||||||
|
throw new ForbiddenError("You don't own this resource.")
|
||||||
|
}
|
||||||
|
return project
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Prisma } from '@prisma/client'
|
import type { Prisma, Project as ProjectType } from '@prisma/client'
|
||||||
import type { ResolverArgs } from '@redwoodjs/api'
|
import type { ResolverArgs } from '@redwoodjs/api'
|
||||||
|
import { uploadImage, makeSocialPublicIdServer } from 'src/lib/cloudinary'
|
||||||
|
|
||||||
import { db } from 'src/lib/db'
|
import { db } from 'src/lib/db'
|
||||||
import {
|
import {
|
||||||
@@ -10,7 +11,7 @@ import {
|
|||||||
destroyImage,
|
destroyImage,
|
||||||
} from 'src/services/helpers'
|
} from 'src/services/helpers'
|
||||||
import { requireAuth } from 'src/lib/auth'
|
import { requireAuth } from 'src/lib/auth'
|
||||||
import { requireOwnership } from 'src/lib/owner'
|
import { requireOwnership, requireProjectOwnership } from 'src/lib/owner'
|
||||||
|
|
||||||
export const projects = ({ userName }) => {
|
export const projects = ({ userName }) => {
|
||||||
if (!userName) {
|
if (!userName) {
|
||||||
@@ -92,12 +93,27 @@ interface UpdateProjectArgs extends Prisma.ProjectWhereUniqueInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const updateProject = async ({ id, input }: UpdateProjectArgs) => {
|
export const updateProject = async ({ id, input }: UpdateProjectArgs) => {
|
||||||
|
const checkSocialCardValidity = async (
|
||||||
|
projectId: string,
|
||||||
|
input: UpdateProjectArgs['input'],
|
||||||
|
oldProject: ProjectType
|
||||||
|
) => {
|
||||||
|
const titleChange = input.title && input.title !== oldProject.title
|
||||||
|
const descriptionChange =
|
||||||
|
input.description && input.description !== oldProject.description
|
||||||
|
if (titleChange || descriptionChange) {
|
||||||
|
return db.socialCard.update({
|
||||||
|
data: { outOfDate: true },
|
||||||
|
where: { projectId },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
requireAuth()
|
requireAuth()
|
||||||
await requireOwnership({ projectId: id })
|
const originalProject = await requireProjectOwnership({ projectId: id })
|
||||||
if (input.title) {
|
if (input.title) {
|
||||||
input.title = enforceAlphaNumeric(input.title)
|
input.title = enforceAlphaNumeric(input.title)
|
||||||
}
|
}
|
||||||
const originalProject = await db.project.findUnique({ where: { id } })
|
const socialCardPromise = checkSocialCardValidity(id, input, originalProject)
|
||||||
const imageToDestroy =
|
const imageToDestroy =
|
||||||
originalProject.mainImage !== input.mainImage &&
|
originalProject.mainImage !== input.mainImage &&
|
||||||
input.mainImage &&
|
input.mainImage &&
|
||||||
@@ -111,11 +127,113 @@ export const updateProject = async ({ id, input }: UpdateProjectArgs) => {
|
|||||||
`image destroyed, publicId: ${imageToDestroy}, projectId: ${id}, replacing image is ${input.mainImage}`
|
`image destroyed, publicId: ${imageToDestroy}, projectId: ${id}, replacing image is ${input.mainImage}`
|
||||||
)
|
)
|
||||||
// destroy after the db has been updated
|
// destroy after the db has been updated
|
||||||
destroyImage({ publicId: imageToDestroy })
|
await destroyImage({ publicId: imageToDestroy })
|
||||||
}
|
}
|
||||||
|
await socialCardPromise
|
||||||
return update
|
return update
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const updateProjectImages = async ({
|
||||||
|
id,
|
||||||
|
mainImage64,
|
||||||
|
socialCard64,
|
||||||
|
}: {
|
||||||
|
id: string
|
||||||
|
mainImage64?: string
|
||||||
|
socialCard64?: string
|
||||||
|
}): Promise<ProjectType> => {
|
||||||
|
requireAuth()
|
||||||
|
const project = await requireProjectOwnership({ projectId: id })
|
||||||
|
const replaceSocialCard = async () => {
|
||||||
|
if (!socialCard64) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let publicId = ''
|
||||||
|
let socialCardId = ''
|
||||||
|
try {
|
||||||
|
;({ id: socialCardId, url: publicId } = await db.socialCard.findUnique({
|
||||||
|
where: { projectId: id },
|
||||||
|
}))
|
||||||
|
} catch (e) {
|
||||||
|
const { userName } = await db.user.findUnique({
|
||||||
|
where: { id: project.userId },
|
||||||
|
})
|
||||||
|
publicId = makeSocialPublicIdServer(userName, project.title)
|
||||||
|
}
|
||||||
|
const imagePromise = uploadImage({
|
||||||
|
image64: socialCard64,
|
||||||
|
uploadPreset: 'CadHub_project_images',
|
||||||
|
publicId,
|
||||||
|
invalidate: true,
|
||||||
|
})
|
||||||
|
const saveOrUpdateSocialCard = () => {
|
||||||
|
const data = {
|
||||||
|
outOfDate: false,
|
||||||
|
url: publicId,
|
||||||
|
}
|
||||||
|
if (socialCardId) {
|
||||||
|
return db.socialCard.update({
|
||||||
|
data,
|
||||||
|
where: { projectId: id },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return db.socialCard.create({
|
||||||
|
data: {
|
||||||
|
...data,
|
||||||
|
project: {
|
||||||
|
connect: {
|
||||||
|
id: id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const socialCardUpdatePromise = saveOrUpdateSocialCard()
|
||||||
|
const [socialCard] = await Promise.all([
|
||||||
|
socialCardUpdatePromise,
|
||||||
|
imagePromise,
|
||||||
|
])
|
||||||
|
return socialCard
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateMainImage = async (): Promise<ProjectType> => {
|
||||||
|
if (!mainImage64) {
|
||||||
|
return project
|
||||||
|
}
|
||||||
|
const { public_id: mainImage } = await uploadImage({
|
||||||
|
image64: mainImage64,
|
||||||
|
uploadPreset: 'CadHub_project_images',
|
||||||
|
invalidate: true,
|
||||||
|
})
|
||||||
|
const projectPromise = db.project.update({
|
||||||
|
data: {
|
||||||
|
mainImage,
|
||||||
|
},
|
||||||
|
where: { id },
|
||||||
|
})
|
||||||
|
let imageDestroyPromise = new Promise((r) => r(null))
|
||||||
|
if (project.mainImage) {
|
||||||
|
console.log(
|
||||||
|
`image destroyed, publicId: ${project.mainImage}, projectId: ${id}, replacing image is ${mainImage}`
|
||||||
|
)
|
||||||
|
// destroy after the db has been updated
|
||||||
|
imageDestroyPromise = destroyImage({ publicId: project.mainImage })
|
||||||
|
}
|
||||||
|
const [updatedProject] = await Promise.all([
|
||||||
|
projectPromise,
|
||||||
|
imageDestroyPromise,
|
||||||
|
])
|
||||||
|
return updatedProject
|
||||||
|
}
|
||||||
|
|
||||||
|
const [updatedProject] = await Promise.all([
|
||||||
|
updateMainImage(),
|
||||||
|
replaceSocialCard(),
|
||||||
|
])
|
||||||
|
|
||||||
|
return updatedProject
|
||||||
|
}
|
||||||
|
|
||||||
export const deleteProject = async ({ id }: Prisma.ProjectWhereUniqueInput) => {
|
export const deleteProject = async ({ id }: Prisma.ProjectWhereUniqueInput) => {
|
||||||
requireAuth()
|
requireAuth()
|
||||||
await requireOwnership({ projectId: id })
|
await requireOwnership({ projectId: id })
|
||||||
|
|||||||
@@ -19,65 +19,6 @@ export const socialCard = ({ id }: Prisma.SocialCardWhereUniqueInput) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = {
|
export const SocialCard = {
|
||||||
project: (_obj, { root }: ResolverArgs<ReturnType<typeof socialCard>>) =>
|
project: (_obj, { root }: ResolverArgs<ReturnType<typeof socialCard>>) =>
|
||||||
db.socialCard.findUnique({ where: { id: root.id } }).project(),
|
db.socialCard.findUnique({ where: { id: root.id } }).project(),
|
||||||
|
|||||||
@@ -5,18 +5,10 @@ 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 { canvasToBlob, blobTo64 } from 'src/helpers/canvasToBlob'
|
||||||
import { useUpdateProject } from 'src/helpers/hooks/useUpdateProject'
|
import { useUpdateProjectImages } from 'src/helpers/hooks/useUpdateProjectImages'
|
||||||
import {
|
|
||||||
useUpdateSocialCard,
|
|
||||||
makeSocialPublicId,
|
|
||||||
} from 'src/helpers/hooks/useUpdateSocialCard'
|
|
||||||
import {
|
|
||||||
uploadToCloudinary,
|
|
||||||
serverVerifiedImageUpload,
|
|
||||||
} from 'src/helpers/cloudinary'
|
|
||||||
import SocialCardCell from 'src/components/SocialCardCell/SocialCardCell'
|
import SocialCardCell from 'src/components/SocialCardCell/SocialCardCell'
|
||||||
import { toJpeg } from 'html-to-image'
|
import { toJpeg } from 'html-to-image'
|
||||||
import { useAuth } from '@redwoodjs/auth'
|
|
||||||
|
|
||||||
const anchorOrigin = {
|
const anchorOrigin = {
|
||||||
vertical: 'bottom',
|
vertical: 'bottom',
|
||||||
@@ -39,24 +31,7 @@ const CaptureButton = ({
|
|||||||
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 ref = React.useRef<HTMLDivElement>(null)
|
||||||
const { updateProject } = useUpdateProject({
|
const { updateProjectImages } = useUpdateProjectImages({})
|
||||||
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
|
||||||
@@ -64,7 +39,7 @@ const CaptureButton = ({
|
|||||||
let imgBlob
|
let imgBlob
|
||||||
let image64
|
let image64
|
||||||
if (!isOpenScadImage) {
|
if (!isOpenScadImage) {
|
||||||
imgBlob = canvasToBlob(threeInstance, { width: 400, height: 300 })
|
imgBlob = canvasToBlob(threeInstance, { width: 500, height: 375 })
|
||||||
image64 = blobTo64(
|
image64 = blobTo64(
|
||||||
await canvasToBlob(threeInstance, { width: 500, height: 522 })
|
await canvasToBlob(threeInstance, { width: 500, height: 522 })
|
||||||
)
|
)
|
||||||
@@ -84,49 +59,45 @@ const CaptureButton = ({
|
|||||||
setCaptureState(config)
|
setCaptureState(config)
|
||||||
|
|
||||||
async function uploadAndUpdateImage() {
|
async function uploadAndUpdateImage() {
|
||||||
const [cloudinaryImgURL, socialCloudinaryURL] = await Promise.all([
|
const upload = async () => {
|
||||||
uploadToCloudinary(config.image),
|
const socialCard64 = toJpeg(ref.current, {
|
||||||
getSocialBlob(),
|
cacheBust: true,
|
||||||
])
|
quality: 0.7,
|
||||||
|
})
|
||||||
|
|
||||||
updateSocialCard({
|
// uploading in two separate mutations because of the 100kb limit of the lambda functions
|
||||||
variables: {
|
const imageUploadPromise1 = updateProjectImages({
|
||||||
projectId: project?.id,
|
variables: {
|
||||||
url: socialCloudinaryURL,
|
id: project?.id,
|
||||||
},
|
mainImage64: await config.image64,
|
||||||
})
|
|
||||||
|
|
||||||
// Save the screenshot as the mainImage
|
|
||||||
updateProject({
|
|
||||||
variables: {
|
|
||||||
id: project?.id,
|
|
||||||
input: {
|
|
||||||
mainImage: cloudinaryImgURL.public_id,
|
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
|
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>,
|
||||||
})
|
})
|
||||||
|
const [{ data }] = await promise
|
||||||
return cloudinaryImgURL
|
return data?.updateProjectImages?.mainImage
|
||||||
}
|
}
|
||||||
|
|
||||||
// if there isn't a screenshot saved yet, just go ahead and save right away
|
// if there isn't a screenshot saved yet, just go ahead and save right away
|
||||||
if (shouldUpdateImage) {
|
if (shouldUpdateImage) {
|
||||||
config.cloudinaryImgURL = (await uploadAndUpdateImage()).public_id
|
config.cloudinaryImgURL = await uploadAndUpdateImage()
|
||||||
config.updated = true
|
config.updated = true
|
||||||
setCaptureState(config)
|
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 }) => {
|
const handleClick = ({ event, whichPopup }) => {
|
||||||
setAnchorEl(event.currentTarget)
|
setAnchorEl(event.currentTarget)
|
||||||
setWhichPopup(whichPopup)
|
setWhichPopup(whichPopup)
|
||||||
@@ -155,59 +126,22 @@ const CaptureButton = ({
|
|||||||
transformOrigin={transformOrigin}
|
transformOrigin={transformOrigin}
|
||||||
className="material-ui-overrides transform translate-y-4"
|
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 ? (
|
{!captureState ? (
|
||||||
'Loading...'
|
'Loading...'
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-2">
|
<div className="">
|
||||||
|
<div className="text-lg">Thumbnail</div>
|
||||||
<div
|
<div
|
||||||
className="rounded m-auto"
|
className="rounded"
|
||||||
style={{ width: 'fit-content', overflow: 'hidden' }}
|
style={{ width: 'fit-content', overflow: 'hidden' }}
|
||||||
>
|
>
|
||||||
<img src={captureState.imageObjectURL} className="w-32" />
|
<img src={captureState.imageObjectURL} className="w-32" />
|
||||||
</div>
|
</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>
|
||||||
)}
|
)}
|
||||||
<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
|
<div
|
||||||
className="transform scale-50 origin-top-left"
|
className="transform scale-50 origin-top-left"
|
||||||
style={{ width: '600px', height: '315px' }}
|
style={{ width: '600px', height: '315px' }}
|
||||||
@@ -221,6 +155,33 @@ const CaptureButton = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { Helmet } from 'react-helmet'
|
|||||||
import { useIsBrowser } from '@redwoodjs/prerender/browserUtils'
|
import { useIsBrowser } from '@redwoodjs/prerender/browserUtils'
|
||||||
|
|
||||||
const Seo = ({
|
const Seo = ({
|
||||||
title = "CadHub",
|
title = 'CadHub',
|
||||||
description = "Edit this part of CadHub",
|
description = 'Edit this part of CadHub',
|
||||||
lang = 'en-US',
|
lang = 'en-US',
|
||||||
socialImageUrl,
|
socialImageUrl,
|
||||||
}: {
|
}: {
|
||||||
@@ -22,25 +22,33 @@ const Seo = ({
|
|||||||
title={title}
|
title={title}
|
||||||
titleTemplate={`Cadhub - ${title}`}
|
titleTemplate={`Cadhub - ${title}`}
|
||||||
>
|
>
|
||||||
<title>{title || 'cadhub'}</title>
|
{title && <title>{title || 'cadhub'}</title>}
|
||||||
<meta name="description" content={description} />
|
{description && <meta name="description" content={description} />}
|
||||||
|
|
||||||
{/* Facebook Meta Tags */}
|
{/* Facebook Meta Tags */}
|
||||||
{browser && <meta property="og:url" content={location.href} />}
|
{browser && <meta property="og:url" content={location.href} />}
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:title" content={title} />
|
{title && <meta property="og:title" content={title} />}
|
||||||
<meta property="og:description" content={description} />
|
{description && (
|
||||||
<meta property="og:image" content={socialImageUrl} />
|
<meta property="og:description" content={description} />
|
||||||
|
)}
|
||||||
|
{socialImageUrl && (
|
||||||
|
<meta property="og:image" content={socialImageUrl} />
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Twitter Meta Tags */}
|
{/* Twitter Meta Tags */}
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<meta property="twitter:domain" content="cadhub.xyz" />
|
<meta property="twitter:domain" content="cadhub.xyz" />
|
||||||
{browser && <meta property="twitter:url" content={location.href} />}
|
{browser && <meta property="twitter:url" content={location.href} />}
|
||||||
<meta name="twitter:title" content={title} />
|
{title && <meta name="twitter:title" content={title} />}
|
||||||
<meta name="twitter:description" content={description} />
|
{description && (
|
||||||
<meta name="twitter:image" content={socialImageUrl} />
|
<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>
|
</Helmet>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ export const Success = ({
|
|||||||
}: CellSuccessProps<FindSocialCardQuery>) => {
|
}: CellSuccessProps<FindSocialCardQuery>) => {
|
||||||
const image = userProject?.Project?.mainImage
|
const image = userProject?.Project?.mainImage
|
||||||
const gravatar = userProject?.image
|
const gravatar = userProject?.image
|
||||||
|
const truncatedDescription =
|
||||||
|
userProject?.Project?.description?.length > 150
|
||||||
|
? (userProject?.Project?.description || '').slice(0, 145) + ' . . .'
|
||||||
|
: userProject?.Project?.description || ''
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex-col flex h-full bg-ch-gray-800 text-ch-gray-300"
|
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' }}
|
style={{ gridTemplateColumns: '7fr 5fr' }}
|
||||||
>
|
>
|
||||||
<div className="bg-ch-gray-800 relative">
|
<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">
|
<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>
|
||||||
{/* <CloudinaryImage
|
|
||||||
cloudName="irevdev"
|
|
||||||
publicId={image || 'CadHub/eia1kwru54g2kf02s2xx'}
|
|
||||||
width={500}
|
|
||||||
crop="scale"
|
|
||||||
/> */}
|
|
||||||
</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="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 justify-between items-center">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
{gravatar && (
|
{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}
|
{userProject?.userName}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<CadPackage
|
<CadPackage
|
||||||
cadPackage={userProject?.Project?.cadPackage}
|
cadPackage={userProject?.Project?.cadPackage}
|
||||||
className="p-2 rounded px-4"
|
className="p-2 rounded px-4 transform scale-150 origin-right"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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, ' ')}
|
{userProject?.Project?.title.replace(/-/g, ' ')}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p className="mt-10 text-2xl font-fira-sans text-ch-gray-400">
|
<p className="mt-10 text-3xl font-fira-sans text-ch-gray-400">
|
||||||
{(userProject?.Project?.description || '').slice(0, 150)}
|
{truncatedDescription}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -104,7 +101,7 @@ export const Success = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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' }}
|
style={{ gridTemplateColumns: '7fr 5fr' }}
|
||||||
>
|
>
|
||||||
<div className="grid grid-flow-col-dense items-center justify-center gap-16">
|
<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) => (
|
].map(({ svg, title, count }, index) => (
|
||||||
<div className="grid grid-flow-col-dense gap-4" key={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="flex flex-col">
|
||||||
<div className="text-3xl">{count}</div>
|
<div className="text-4xl">{count}</div>
|
||||||
<div className="text-xl text-ch-gray-400">{title}</div>
|
<div className="text-3xl text-ch-gray-400">{title}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -134,17 +131,11 @@ export const Success = ({
|
|||||||
<div className="ml-2 md:ml-6 flex">
|
<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. */}
|
{/* 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
|
<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' }}
|
style={{ letterSpacing: '0.3em' }}
|
||||||
>
|
>
|
||||||
CadHub
|
CadHub
|
||||||
</h2>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -199,9 +199,13 @@ function getParams(target: HTMLElement): RawCustomizerParams {
|
|||||||
if (
|
if (
|
||||||
numeric[elem.getAttribute('type')] ||
|
numeric[elem.getAttribute('type')] ||
|
||||||
elem.getAttribute('numeric') == '1'
|
elem.getAttribute('numeric') == '1'
|
||||||
){
|
) {
|
||||||
value = parseFloat(String(value || 0))
|
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))
|
value = parseFloat(String(value || 0))
|
||||||
}
|
}
|
||||||
if (elem.type == 'radio' && !elem.checked) return // skip if not checked radio button
|
if (elem.type == 'radio' && !elem.checked) return // skip if not checked radio button
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export const canvasToBlob = async (
|
|||||||
resolve(blob)
|
resolve(blob)
|
||||||
},
|
},
|
||||||
'image/jpeg',
|
'image/jpeg',
|
||||||
1
|
0.75
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
updateCanvasSize(oldSize)
|
updateCanvasSize(oldSize)
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
36
app/web/src/helpers/hooks/useUpdateProjectImages.ts
Normal file
36
app/web/src/helpers/hooks/useUpdateProjectImages.ts
Normal 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}`
|
||||||
@@ -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}`
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import IdeProjectCell from 'src/components/IdeProjectCell'
|
import IdeProjectCell from 'src/components/IdeProjectCell'
|
||||||
import Seo from 'src/components/Seo/Seo'
|
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 IdeProjectPage = ({ userName, projectTitle }) => {
|
||||||
const socialImageUrl = `http://res.cloudinary.com/irevdev/image/upload/c_scale,w_1200/v1/CadHub/${makeSocialPublicId(
|
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 (
|
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} />
|
<IdeProjectCell userName={userName} projectTitle={projectTitle} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ 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'
|
import { makeSocialPublicId } from 'src/helpers/hooks/useUpdateProjectImages'
|
||||||
|
|
||||||
const ProjectPage = ({ userName, projectTitle }) => {
|
const ProjectPage = ({ userName, projectTitle }) => {
|
||||||
const { currentUser } = useAuth()
|
const { currentUser } = useAuth()
|
||||||
|
|||||||
Reference in New Issue
Block a user