From e4bf8f5e81799608fe59ce2788a21bfe14a99216 Mon Sep 17 00:00:00 2001 From: Kurt Hutten Date: Fri, 30 Jul 2021 21:04:28 +1000 Subject: [PATCH 1/7] Add social preview page --- app/web/src/Routes.js | 1 + app/web/src/components/Gravatar/Gravatar.tsx | 5 +- .../src/components/IdeWrapper/useSaveCode.ts | 2 +- .../SocialCardCell/SocialCardCell.tsx | 144 ++++++++++++++++++ app/web/src/components/Svg/Svg.tsx | 49 ++++++ .../pages/SocialCardPage/SocialCardPage.tsx | 13 ++ app/web/tailwind.config.js | 2 + 7 files changed, 213 insertions(+), 3 deletions(-) create mode 100644 app/web/src/components/SocialCardCell/SocialCardCell.tsx create mode 100644 app/web/src/pages/SocialCardPage/SocialCardPage.tsx diff --git a/app/web/src/Routes.js b/app/web/src/Routes.js index 665ad9d..4279d72 100644 --- a/app/web/src/Routes.js +++ b/app/web/src/Routes.js @@ -55,6 +55,7 @@ const Routes = () => { + diff --git a/app/web/src/components/Gravatar/Gravatar.tsx b/app/web/src/components/Gravatar/Gravatar.tsx index 58ea606..08603f5 100644 --- a/app/web/src/components/Gravatar/Gravatar.tsx +++ b/app/web/src/components/Gravatar/Gravatar.tsx @@ -4,9 +4,10 @@ import { Image as CloudinaryImage } from 'cloudinary-react' interface Props { image: string className?: string + size?: number } -const Gravatar = ({ image, className = '' }: Props) => { +const Gravatar = ({ image, size = 40, className = '' }: Props) => { return (
{
diff --git a/app/web/src/components/IdeWrapper/useSaveCode.ts b/app/web/src/components/IdeWrapper/useSaveCode.ts index 8a26161..69dcd71 100644 --- a/app/web/src/components/IdeWrapper/useSaveCode.ts +++ b/app/web/src/components/IdeWrapper/useSaveCode.ts @@ -19,7 +19,7 @@ export const useSaveCode = () => { setNowError(!!error) } if (!currentUser || project?.user?.id !== currentUser?.sub) { - return () => console.log('not your project') + return () => {} } return (input: Prisma.ProjectUpdateInput) => { updateProject({ variables: { id: project.id, input } }) diff --git a/app/web/src/components/SocialCardCell/SocialCardCell.tsx b/app/web/src/components/SocialCardCell/SocialCardCell.tsx new file mode 100644 index 0000000..8182221 --- /dev/null +++ b/app/web/src/components/SocialCardCell/SocialCardCell.tsx @@ -0,0 +1,144 @@ +import type { FindSocialCardQuery } from 'types/graphql' +import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web' +import Svg from 'src/components/Svg/Svg' +import { Image as CloudinaryImage } from 'cloudinary-react' +import Gravatar from 'src/components/Gravatar/Gravatar' +import CadPackage from 'src/components/CadPackage/CadPackage' + +export const QUERY = gql` + query FindSocialCardQuery($userName: String!, $projectTitle: String) { + userProject: userName(userName: $userName) { + userName + image + Project(projectTitle: $projectTitle) { + id + title + description + mainImage + createdAt + updatedAt + userId + cadPackage + Reaction { + emote + } + } + } + } +` + +export const Loading = () =>
Loading...
+ +export const Empty = () =>
Empty
+ +export const Failure = ({ error }: CellFailureProps) => ( +
Error: {error.message}
+) + +export const Success = ({ + userProject, +}: CellSuccessProps) => { + const image = userProject?.Project?.mainImage + const gravatar = userProject?.image + return ( +
+
+
+
+ {/* */} +
+ +
+
+
+ {gravatar && ( + + )} +
+ {userProject?.userName} +
+
+ +
+ +

+ {userProject?.Project?.title.replace(/-/g, ' ')} +

+ +

+ {(userProject?.Project?.description || '').slice(0, 150)} +

+
+
+
+
+ +
+
+
+
+
+ {[ + { + svg: 'reactions', + title: 'Reactions', + count: userProject?.Project?.Reaction?.length, + }, + { + svg: 'fork-new', + title: 'Forks', + count: 0, + }, + ].map(({ svg, title, count }, index) => ( +
+ +
+
{count}
+
{title}
+
+
+ ))} +
+
+ +
+ {/* 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. */} +

+ CadHub +

+
+ pre-alpha +
+
+
+
+
+
+ ) +} diff --git a/app/web/src/components/Svg/Svg.tsx b/app/web/src/components/Svg/Svg.tsx index 2313dff..f21612d 100644 --- a/app/web/src/components/Svg/Svg.tsx +++ b/app/web/src/components/Svg/Svg.tsx @@ -13,6 +13,7 @@ type SvgNames = | 'flag' | 'floppy-disk' | 'fork' + | 'fork-new' | 'gear' | 'lightbulb' | 'logout' @@ -22,6 +23,7 @@ type SvgNames = | 'photograph' | 'plus' | 'plus-circle' + | 'reactions' | 'refresh' | 'save' | 'share' @@ -294,6 +296,39 @@ const Svg = ({ /> ), + 'fork-new': ( + + + + + + + ), gear: ( ), + reactions: ( + + + + + ), refresh: ( { + return +} + +export default SocialCardPage diff --git a/app/web/tailwind.config.js b/app/web/tailwind.config.js index e492a68..e1dca04 100644 --- a/app/web/tailwind.config.js +++ b/app/web/tailwind.config.js @@ -22,6 +22,7 @@ module.exports = { 700: '#2A3038', 600: '#3B3E4B', 500: '#9F9FB4', + 400: '#A4A4B0', 300: '#CFCFD8', }, 'ch-purple': { @@ -31,6 +32,7 @@ module.exports = { }, 'ch-blue': { 600: '#79B2F8', + 500: '5098F1', 300: '#08466F' }, 'ch-pink': { From cbaa79b697a06c658a392cbad02392b73c5f41ec Mon Sep 17 00:00:00 2001 From: Kurt Hutten Date: Sat, 31 Jul 2021 15:25:30 +1000 Subject: [PATCH 2/7] add netlify builder to capture social images plus also added an invalidator that should invalidate the image each month --- app/api/package.json | 6 +- app/api/src/functions/og-image-generator.ts | 63 +++++++ .../src/components/Seo/{Seo.js => Seo.tsx} | 3 +- .../SocialCardCell/SocialCardCell.tsx | 4 +- .../src/pages/NotFoundPage/NotFoundPage.js | 1 + app/web/src/pages/ProjectPage/ProjectPage.tsx | 4 +- app/yarn.lock | 176 ++++++++++++++++-- 7 files changed, 238 insertions(+), 19 deletions(-) create mode 100644 app/api/src/functions/og-image-generator.ts rename app/web/src/components/Seo/{Seo.js => Seo.tsx} (71%) diff --git a/app/api/package.json b/app/api/package.json index b56a4c1..a57d377 100644 --- a/app/api/package.json +++ b/app/api/package.json @@ -3,11 +3,15 @@ "version": "0.0.0", "private": true, "dependencies": { + "@netlify/functions": "^0.7.2", "@redwoodjs/api": "^0.34.1", "@sentry/node": "^6.5.1", + "chrome-aws-lambda": "^10.1.0", "cloudinary": "^1.23.0", "human-id": "^2.0.1", - "nodemailer": "^6.6.2" + "nodemailer": "^6.6.2", + "puppeteer": "^10.1.0", + "puppeteer-core": "^10.1.0" }, "devDependencies": { "@types/nodemailer": "^6.4.2" diff --git a/app/api/src/functions/og-image-generator.ts b/app/api/src/functions/og-image-generator.ts new file mode 100644 index 0000000..d82b84b --- /dev/null +++ b/app/api/src/functions/og-image-generator.ts @@ -0,0 +1,63 @@ +import { builder } from '@netlify/functions' +import type { HandlerResponse } from '@netlify/functions' +import chromium from 'chrome-aws-lambda' + +const captureWidth = 1200 +const captureHeight = 630 +const clipY = 0 + +async function unwrappedHandler (event, context): Promise { + let path = event.path + .replace(/.+\/og-image-generator/, '') + .replace(/\/og-image-.+\.jpg/, '') + + const url = `${process.env.URL}/u${path}/social-card` + + const browser = await chromium.puppeteer.launch({ + executablePath: process.env.URL?.includes('localhost') + ? null + : await chromium.executablePath, + args: ['--no-sandbox','--disable-web-security','--disable-gpu', '--hide-scrollbars', '--disable-setuid-sandbox'], + // args: chromium.args, + defaultViewport: { + width: captureWidth, + height: captureHeight + clipY + }, + headless: chromium.headless + }) + const page = await browser.newPage() + + await page.goto(url, {"waitUntil" : "networkidle0"}); + + const screenshot = await page.screenshot({ + type: 'jpeg', + // netlify functions can only return strings, so base64 it is + encoding: 'base64', + quality: 70, + clip: { + x: 0, + y: clipY, + width: captureWidth, + height: captureHeight + } + }) + + await browser.close() + + if (typeof screenshot !== 'string') { + return { + statusCode: 400, + } + } + + return { + statusCode: 200, + headers: { + 'Content-Type': 'image/jpg' + }, + body: screenshot, + isBase64Encoded: true + } +} + +export const handler = builder(unwrappedHandler) diff --git a/app/web/src/components/Seo/Seo.js b/app/web/src/components/Seo/Seo.tsx similarity index 71% rename from app/web/src/components/Seo/Seo.js rename to app/web/src/components/Seo/Seo.tsx index 69d97f0..1697b8d 100644 --- a/app/web/src/components/Seo/Seo.js +++ b/app/web/src/components/Seo/Seo.tsx @@ -1,6 +1,6 @@ import { Helmet } from 'react-helmet' -const Seo = ({ title, description, lang }) => { +const Seo = ({ title, description, lang, socialImageUrl}: { title: string; description: string; lang: string; socialImageUrl?: string}) => { return ( <> { + Cadhub - {title} diff --git a/app/web/src/components/SocialCardCell/SocialCardCell.tsx b/app/web/src/components/SocialCardCell/SocialCardCell.tsx index 8182221..01ee464 100644 --- a/app/web/src/components/SocialCardCell/SocialCardCell.tsx +++ b/app/web/src/components/SocialCardCell/SocialCardCell.tsx @@ -41,7 +41,7 @@ export const Success = ({ const image = userProject?.Project?.mainImage const gravatar = userProject?.image return ( -
+
-
+
(

404 Page Not Found +
{location.href} 🤷

diff --git a/app/web/src/pages/ProjectPage/ProjectPage.tsx b/app/web/src/pages/ProjectPage/ProjectPage.tsx index 7e401ca..24577be 100644 --- a/app/web/src/pages/ProjectPage/ProjectPage.tsx +++ b/app/web/src/pages/ProjectPage/ProjectPage.tsx @@ -9,9 +9,11 @@ import { Toaster } from '@redwoodjs/web/toast' 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` return ( <> - + Date: Sat, 31 Jul 2021 17:04:20 +1000 Subject: [PATCH 3/7] attempt to got functions to deploy --- app/api/src/functions/og-image-generator.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/api/src/functions/og-image-generator.ts b/app/api/src/functions/og-image-generator.ts index d82b84b..4a50e47 100644 --- a/app/api/src/functions/og-image-generator.ts +++ b/app/api/src/functions/og-image-generator.ts @@ -1,6 +1,6 @@ import { builder } from '@netlify/functions' import type { HandlerResponse } from '@netlify/functions' -import chromium from 'chrome-aws-lambda' +const { headless, executablePath, puppeteer} = require('chrome-aws-lambda') const captureWidth = 1200 const captureHeight = 630 @@ -13,17 +13,17 @@ async function unwrappedHandler (event, context): Promise { const url = `${process.env.URL}/u${path}/social-card` - const browser = await chromium.puppeteer.launch({ + const browser = await puppeteer.launch({ executablePath: process.env.URL?.includes('localhost') ? null - : await chromium.executablePath, + : await executablePath, args: ['--no-sandbox','--disable-web-security','--disable-gpu', '--hide-scrollbars', '--disable-setuid-sandbox'], // args: chromium.args, defaultViewport: { width: captureWidth, height: captureHeight + clipY }, - headless: chromium.headless + headless: headless }) const page = await browser.newPage() From 6d68b939bebd3e875a14d8f7298ac22246c6544b Mon Sep 17 00:00:00 2001 From: Kurt Hutten Date: Sat, 31 Jul 2021 17:26:10 +1000 Subject: [PATCH 4/7] try moving things into dev dependencies --- app/api/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/api/package.json b/app/api/package.json index a57d377..354a796 100644 --- a/app/api/package.json +++ b/app/api/package.json @@ -3,17 +3,17 @@ "version": "0.0.0", "private": true, "dependencies": { - "@netlify/functions": "^0.7.2", "@redwoodjs/api": "^0.34.1", "@sentry/node": "^6.5.1", "chrome-aws-lambda": "^10.1.0", "cloudinary": "^1.23.0", "human-id": "^2.0.1", "nodemailer": "^6.6.2", - "puppeteer": "^10.1.0", "puppeteer-core": "^10.1.0" }, "devDependencies": { + "@netlify/functions": "^0.7.2", + "puppeteer": "^10.1.0", "@types/nodemailer": "^6.4.2" } } From 22f3fb6e3e0f895e1051cc758ddc424b674898d7 Mon Sep 17 00:00:00 2001 From: Kurt Hutten Date: Sun, 1 Aug 2021 04:45:43 +1000 Subject: [PATCH 5/7] Remove og image types --- app/api/src/functions/og-image-generator.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/api/src/functions/og-image-generator.ts b/app/api/src/functions/og-image-generator.ts index 4a50e47..791a27c 100644 --- a/app/api/src/functions/og-image-generator.ts +++ b/app/api/src/functions/og-image-generator.ts @@ -1,12 +1,11 @@ import { builder } from '@netlify/functions' -import type { HandlerResponse } from '@netlify/functions' const { headless, executablePath, puppeteer} = require('chrome-aws-lambda') const captureWidth = 1200 const captureHeight = 630 const clipY = 0 -async function unwrappedHandler (event, context): Promise { +async function unwrappedHandler (event, context) { let path = event.path .replace(/.+\/og-image-generator/, '') .replace(/\/og-image-.+\.jpg/, '') From caf944716b9e23ba254a73387d212d98f16c9bdf Mon Sep 17 00:00:00 2001 From: Kurt Hutten Date: Sun, 1 Aug 2021 09:42:15 +1000 Subject: [PATCH 6/7] Move og-image-gen out of functions folder --- app/api/src/{functions => services}/og-image-generator.ts | 3 +++ 1 file changed, 3 insertions(+) rename app/api/src/{functions => services}/og-image-generator.ts (85%) diff --git a/app/api/src/functions/og-image-generator.ts b/app/api/src/services/og-image-generator.ts similarity index 85% rename from app/api/src/functions/og-image-generator.ts rename to app/api/src/services/og-image-generator.ts index 791a27c..f5877c6 100644 --- a/app/api/src/functions/og-image-generator.ts +++ b/app/api/src/services/og-image-generator.ts @@ -1,3 +1,6 @@ +// TODO this should be in the functions folder. +// Got the proof of concept working locally, but even though chrome-aws-lambda is supposed to fit into a AWS lambda it did not for me +// in the mean time this is causing builds to fail so moved it out here. import { builder } from '@netlify/functions' const { headless, executablePath, puppeteer} = require('chrome-aws-lambda') From ad0e9c1d4dae97ba3227dc795b59c0bc34ea9f0e Mon Sep 17 00:00:00 2001 From: Kurt Hutten Date: Sun, 1 Aug 2021 09:44:10 +1000 Subject: [PATCH 7/7] format project --- app/api/src/services/og-image-generator.ts | 28 +++++++++++-------- app/web/src/components/Seo/Seo.tsx | 12 +++++++- .../SocialCardCell/SocialCardCell.tsx | 5 +++- app/web/src/pages/ProjectPage/ProjectPage.tsx | 13 +++++++-- 4 files changed, 43 insertions(+), 15 deletions(-) diff --git a/app/api/src/services/og-image-generator.ts b/app/api/src/services/og-image-generator.ts index f5877c6..93adbdd 100644 --- a/app/api/src/services/og-image-generator.ts +++ b/app/api/src/services/og-image-generator.ts @@ -2,14 +2,14 @@ // Got the proof of concept working locally, but even though chrome-aws-lambda is supposed to fit into a AWS lambda it did not for me // in the mean time this is causing builds to fail so moved it out here. import { builder } from '@netlify/functions' -const { headless, executablePath, puppeteer} = require('chrome-aws-lambda') +const { headless, executablePath, puppeteer } = require('chrome-aws-lambda') const captureWidth = 1200 const captureHeight = 630 const clipY = 0 -async function unwrappedHandler (event, context) { - let path = event.path +async function unwrappedHandler(event, context) { + const path = event.path .replace(/.+\/og-image-generator/, '') .replace(/\/og-image-.+\.jpg/, '') @@ -19,17 +19,23 @@ async function unwrappedHandler (event, context) { executablePath: process.env.URL?.includes('localhost') ? null : await executablePath, - args: ['--no-sandbox','--disable-web-security','--disable-gpu', '--hide-scrollbars', '--disable-setuid-sandbox'], + args: [ + '--no-sandbox', + '--disable-web-security', + '--disable-gpu', + '--hide-scrollbars', + '--disable-setuid-sandbox', + ], // args: chromium.args, defaultViewport: { width: captureWidth, - height: captureHeight + clipY + height: captureHeight + clipY, }, - headless: headless + headless: headless, }) const page = await browser.newPage() - await page.goto(url, {"waitUntil" : "networkidle0"}); + await page.goto(url, { waitUntil: 'networkidle0' }) const screenshot = await page.screenshot({ type: 'jpeg', @@ -40,8 +46,8 @@ async function unwrappedHandler (event, context) { x: 0, y: clipY, width: captureWidth, - height: captureHeight - } + height: captureHeight, + }, }) await browser.close() @@ -55,10 +61,10 @@ async function unwrappedHandler (event, context) { return { statusCode: 200, headers: { - 'Content-Type': 'image/jpg' + 'Content-Type': 'image/jpg', }, body: screenshot, - isBase64Encoded: true + isBase64Encoded: true, } } diff --git a/app/web/src/components/Seo/Seo.tsx b/app/web/src/components/Seo/Seo.tsx index 1697b8d..7c8adc5 100644 --- a/app/web/src/components/Seo/Seo.tsx +++ b/app/web/src/components/Seo/Seo.tsx @@ -1,6 +1,16 @@ import { Helmet } from 'react-helmet' -const Seo = ({ title, description, lang, socialImageUrl}: { title: string; description: string; lang: string; socialImageUrl?: string}) => { +const Seo = ({ + title, + description, + lang, + socialImageUrl, +}: { + title: string + description: string + lang: string + socialImageUrl?: string +}) => { return ( <> +
{ const { currentUser } = useAuth() const [state, thunkDispatch] = useIdeState() - const cacheInvalidator = new Date().toISOString().split('-').slice(0, 2).join('-') + const cacheInvalidator = new Date() + .toISOString() + .split('-') + .slice(0, 2) + .join('-') const socialImageUrl = `/.netlify/functions/og-image-generator/${userName}/${projectTitle}/og-image-${cacheInvalidator}.jpg` return ( <> - +