-
Notifications
You must be signed in to change notification settings - Fork 65
feat: twitter share #1642
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
feat: twitter share #1642
Changes from all commits
Commits
Show all changes
47 commits
Select commit
Hold shift + click to select a range
577c14b
add canvas dep
gsteenkamp89 1262125
draw base layer image, serve with page & necessary tags
gsteenkamp89 34297ad
add unstyled component. build URL
gsteenkamp89 47f4f6c
style banner
gsteenkamp89 dad022d
fixup
gsteenkamp89 0612b9b
fix
gsteenkamp89 1e2bab7
fixes
gsteenkamp89 2c8fb3e
add transitive system deps for canvas
gsteenkamp89 6d31ea4
add canvas as a dev dependency
gsteenkamp89 d2b7406
test new dimensions
gsteenkamp89 e9cda1a
square image
gsteenkamp89 47550f7
serve better html
gsteenkamp89 52307bb
draw chain logos, cache, add redirect link
gsteenkamp89 7451509
fixup
gsteenkamp89 9b248a3
serve content based on user agent
gsteenkamp89 d3a101c
remove some duplicate icons
gsteenkamp89 a5cf7e2
use even 20px padding
gsteenkamp89 211a146
refactor
gsteenkamp89 a2a5d4e
update copy
gsteenkamp89 f5859cc
handle 0 seconds bridge
gsteenkamp89 9d59695
fixup
gsteenkamp89 52b1b5d
update UI copy
gsteenkamp89 4b3f186
add utm params to redirect
gsteenkamp89 d816bee
Merge branch 'master' into feat/twitter-share
gsteenkamp89 f543e56
fix tweet text
gsteenkamp89 eed9e29
fix aspect ratio
gsteenkamp89 96ab4c8
Merge branch 'master' into feat/twitter-share
gsteenkamp89 c5af293
update lock file
gsteenkamp89 a750b91
fixup
gsteenkamp89 672e9eb
update card styles, create base modal
gsteenkamp89 d0dd834
style modal
gsteenkamp89 a33ba86
update images
gsteenkamp89 c791255
add copy and download functionality
gsteenkamp89 42ffed3
fixup
gsteenkamp89 6b3494f
add animated copy button
gsteenkamp89 e2d07e4
Merge branch 'master' into feat/twitter-share
gsteenkamp89 73a0a91
create distinct frontend and api test environments
gsteenkamp89 92dc255
Merge branch 'master' into feat/twitter-share
gsteenkamp89 c31d838
test: rebuild canvas
james-a-morris 439e2ff
use ready as default state
gsteenkamp89 1e86580
Merge branch 'master' into feat/twitter-share
gsteenkamp89 ee831c5
increase image scale
gsteenkamp89 1e5f0e1
fixup
gsteenkamp89 1a17be7
dynamic copy based on env var
gsteenkamp89 4571cca
edit copy
gsteenkamp89 f58fd1e
add solana icon
gsteenkamp89 938532b
update solana icon
gsteenkamp89 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import { getChainInfo } from "../_utils"; | ||
| import path from "path"; | ||
| import fs from "fs"; | ||
|
|
||
| const assetsDir = path.join(__dirname, "assets"); | ||
|
|
||
| export function getChainLogoPath(chainId: number): string | undefined { | ||
| try { | ||
| const chainName = getChainInfo(chainId) | ||
| .name.toLowerCase() | ||
| .replaceAll(" ", "-"); | ||
| const logoPath = path.join(assetsDir, "chain-logos", `${chainName}.png`); | ||
|
|
||
| if (!fs.existsSync(logoPath)) { | ||
| throw new Error(`No chain logo found at path ${logoPath}`); | ||
| } | ||
|
|
||
| return logoPath; | ||
| } catch (e) { | ||
| console.warn(`Unable to find logo for chainId ${chainId}.`, { | ||
| cause: e, | ||
| chainId, | ||
| }); | ||
| return undefined; | ||
| } | ||
| } |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| import { VercelResponse } from "@vercel/node"; | ||
| import { createCanvas, loadImage } from "canvas"; | ||
| import { TypedVercelRequest } from "../_types"; | ||
| import { handleErrorCondition } from "../_errors"; | ||
| import { getLogger, intStringInRange, positiveIntStr } from "../_utils"; | ||
| import path from "path"; | ||
|
|
||
| import { assert, Infer, type } from "superstruct"; | ||
| import { getChainLogoPath } from "./_utils"; | ||
| const assetsDir = path.join(__dirname, "assets"); | ||
|
|
||
| const TwitterShareParamsSchema = type({ | ||
| fill_time: intStringInRange(0, 5), | ||
| from_chain: positiveIntStr(), | ||
| to_chain: positiveIntStr(), | ||
| }); | ||
|
|
||
| export type TwitterShareImageParams = Infer<typeof TwitterShareParamsSchema>; | ||
|
|
||
| export const CANVAS = { | ||
| width: 800, | ||
| height: 800, | ||
| }; | ||
|
|
||
| const chainLogoDimensions = { | ||
| x: 89, | ||
| y: 89, | ||
| }; | ||
|
|
||
| export default async function handler( | ||
| request: TypedVercelRequest<TwitterShareImageParams>, | ||
| response: VercelResponse | ||
| ) { | ||
| const logger = getLogger(); | ||
| try { | ||
| logger.debug({ | ||
| at: "api/twitter-share", | ||
| message: "Query data", | ||
| query: request.query, | ||
| }); | ||
| assert(request.query, TwitterShareParamsSchema); | ||
| const { fill_time, from_chain, to_chain } = request.query; | ||
| const canvas = createCanvas(CANVAS.width, CANVAS.height); | ||
| const ctx = canvas.getContext("2d"); | ||
|
|
||
| const baseImage = await loadImage( | ||
| path.join(assetsDir, "base", `${fill_time}.png`) | ||
| ); | ||
|
|
||
| // Get chain logo paths and load images only if they exist | ||
| const originChainLogoPath = getChainLogoPath(Number(from_chain)); | ||
| const destinationChainLogoPath = getChainLogoPath(Number(to_chain)); | ||
|
|
||
| const [originChainLogo, destinationChainLogo] = await Promise.all([ | ||
| originChainLogoPath ? loadImage(originChainLogoPath) : null, | ||
| destinationChainLogoPath ? loadImage(destinationChainLogoPath) : null, | ||
| ]); | ||
|
|
||
| // draw base layer with padding | ||
| ctx.drawImage(baseImage, 0, 0, CANVAS.width, CANVAS.height); | ||
|
|
||
| // Draw chain logos only if they exist (base layer contains fallback) | ||
| if (originChainLogo) { | ||
| ctx.drawImage( | ||
| originChainLogo, | ||
| 182, | ||
| 356, | ||
| chainLogoDimensions.x, | ||
| chainLogoDimensions.y | ||
| ); | ||
| } | ||
| if (destinationChainLogo) { | ||
| ctx.drawImage( | ||
| destinationChainLogo, | ||
| 529, | ||
| 356, | ||
| chainLogoDimensions.x, | ||
| chainLogoDimensions.y | ||
| ); | ||
| } | ||
|
|
||
| response.setHeader( | ||
| "Cache-Control", | ||
| `public, max-age=${60 * 60 * 24 * 30}, immutable` | ||
| ); // 1 month | ||
| response.setHeader("Content-Type", "image/png"); | ||
|
|
||
| const stream = canvas.createPNGStream(); | ||
|
|
||
| // Handle stream errors | ||
| stream.on("error", (streamErr) => { | ||
| logger.error({ | ||
| at: "api/twitter-share", | ||
| message: "PNG stream error", | ||
| error: streamErr, | ||
| }); | ||
| if (!response.headersSent) { | ||
| response.status(500).end(); | ||
| } | ||
| }); | ||
|
|
||
| stream.pipe(response); | ||
| } catch (err) { | ||
| logger.error({ | ||
| at: "api/twitter-share", | ||
| message: "Failed to generate Twitter share image", | ||
| error: err, | ||
| }); | ||
|
|
||
| // Only handle error if response hasn't been sent yet | ||
| if (!response.headersSent) { | ||
| handleErrorCondition("twitter-share-image", response, logger, err); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| /** @type {import('ts-jest/dist/types').JestConfigWithTsJest} */ | ||
|
|
||
| module.exports = { | ||
| setupFiles: ["<rootDir>/setup.jest.ts"], | ||
| preset: "ts-jest", | ||
| testEnvironment: "jsdom", | ||
| moduleDirectories: ["node_modules", "<rootDir>"], | ||
| moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], | ||
| transform: { | ||
| "^.+\\.tsx?$": "ts-jest", | ||
| "^.+\\.svg$": "jest-transform-stub", | ||
| "^.+\\.png$": "jest-transform-stub", | ||
| }, | ||
| // Only include src directory for frontend tests | ||
| testMatch: ["<rootDir>/src/**/*.test.{ts,tsx}"], | ||
| // Exclude API modules that require Node.js dependencies | ||
| modulePathIgnorePatterns: ["<rootDir>/api/", "<rootDir>/test/api/"], | ||
| moduleNameMapper: { | ||
| "^components/(.*)$": "<rootDir>/src/components/$1", | ||
| "^utils/(.*)$": "<rootDir>/src/utils/$1", | ||
| "^hooks/(.*)$": "<rootDir>/src/hooks/$1", | ||
| "^assets/(.*)$": "<rootDir>/src/assets/$1", | ||
| "^data/(.*)$": "<rootDir>/src/data/$1", | ||
| uuid: require.resolve("uuid"), | ||
| }, | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Diff not rendered.
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
Diff not rendered.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will this be an issue on vercel?