Skip to content

Commit

Permalink
Added OG Image (#5251)
Browse files Browse the repository at this point in the history
- Added dynamic OG Image to share and download in contributors page

<img width="1176" alt="Screenshot 2024-05-02 at 16 24 00"
src="https://github.com/twentyhq/twenty/assets/102751374/0579454b-ccc7-46ba-9875-52458f06ee82">

- Added dynamic metadata 

- Added design to contributor page

- Added a NEXT_PUBLIC_HOST_URL in the .env file

Co-authored-by: Ady Beraud <[email protected]>
  • Loading branch information
ady-beraud and ady-test authored May 3, 2024
1 parent a5a9e0e commit 2067069
Show file tree
Hide file tree
Showing 6 changed files with 507 additions and 106 deletions.
1 change: 1 addition & 0 deletions packages/twenty-website/.env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
GITHUB_TOKEN=your_github_token
DATABASE_PG_URL=postgres://website:website@localhost:5432/website # only if using postgres
NEXT_PUBLIC_HOST_URL=http://localhost:3000

Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
'use client';

import styled from '@emotion/styled';
import { IconDownload } from '@tabler/icons-react';

import { CardContainer } from '@/app/_components/contributors/CardContainer';
import { GithubIcon, XIcon } from '@/app/_components/ui/icons/SvgIcons';
import { Theme } from '@/app/_components/ui/theme/theme';

const Container = styled(CardContainer)`
flex-direction: row;
justify-content: center;
align-items: baseline;
gap: 32px;
padding: 40px 0px;
@media (min-width: 800px) and (max-width: 855px) {
padding: 40px 24px;
gap: 24px;
}
@media (max-width: 800px) {
flex-direction: column;
align-items: center;
}
`;

const StyledButton = styled.a`
display: flex;
flex-direction: row;
font-size: ${Theme.font.size.lg};
font-weight: ${Theme.font.weight.medium};
padding: 14px 24px;
color: ${Theme.text.color.primary};
font-family: var(--font-gabarito);
background-color: #fafafa;
border: 2px solid ${Theme.color.gray60};
border-radius: 12px;
gap: 12px;
cursor: pointer;
text-decoration: none;
box-shadow:
-6px 6px 0px 1px #fafafa,
-6px 6px 0px 3px ${Theme.color.gray60};
&:hover {
box-shadow: -6px 6px 0px 1px ${Theme.color.gray60};
}
`;

interface ProfileProps {
userUrl: string;
username: string;
}

export const ProfileSharing = ({ userUrl, username }: ProfileProps) => {
const contributorUrl = `${process.env.NEXT_PUBLIC_HOST_URL}/contributors/${username}`;

const handleDownload = async () => {
const imageSrc = `${process.env.NEXT_PUBLIC_HOST_URL}/api/contributors/og-image/${username}`;
try {
const response = await fetch(imageSrc);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);

const a = document.createElement('a');
a.href = url;
a.download = `twenty-${username}.png`;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
} catch (error) {
console.error('Error downloading the image:', error);
}
};

return (
<Container>
<StyledButton href={userUrl} target="blank">
<GithubIcon color="black" size="24px" />
Visit Profile
</StyledButton>
<StyledButton onClick={handleDownload}>
<IconDownload /> Download Image
</StyledButton>
<StyledButton
href={`http://www.twitter.com/share?url=${contributorUrl}`}
target="blank"
>
<XIcon color="black" size="24px" /> Share on X
</StyledButton>
</Container>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { format } from 'date-fns';
import { ImageResponse } from 'next/og';

import {
bottomBackgroundImage,
container,
contributorInfo,
contributorInfoBox,
contributorInfoContainer,
contributorInfoStats,
contributorInfoTitle,
infoSeparator,
profileContainer,
profileContributionHeader,
profileInfoContainer,
profileUsernameHeader,
styledContributorAvatar,
topBackgroundImage,
} from '@/app/api/contributors/og-image/[slug]/style';
import { getContributorActivity } from '@/app/contributors/utils/get-contributor-activity';

const GABARITO_FONT_CDN_URL =
'https://fonts.cdnfonts.com/s/105143/Gabarito-Medium-BF651cdf1f3f18e.woff';

const getGabarito = async () => {
const fontGabarito = await fetch(GABARITO_FONT_CDN_URL).then((res) =>
res.arrayBuffer(),
);

return fontGabarito;
};

export async function GET(request: Request) {
try {
const url = request.url;

const username = url.split('/')?.pop() || '';

const contributorActivity = await getContributorActivity(username);
if (contributorActivity) {
const {
firstContributionAt,
mergedPRsCount,
rank,
activeDays,
contributorAvatar,
} = contributorActivity;
return await new ImageResponse(
(
<div style={container}>
<div style={topBackgroundImage}></div>
<div style={bottomBackgroundImage}></div>
<div style={profileContainer}>
<img src={contributorAvatar} style={styledContributorAvatar} />
<div style={profileInfoContainer}>
<h1 style={profileUsernameHeader}>@{username} x Twenty</h1>
<h2 style={profileContributionHeader}>
Since {format(new Date(firstContributionAt), 'MMMM yyyy')}
</h2>
</div>
<svg
width="96"
height="96"
viewBox="0 0 136 136"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_2343_96406)">
<path
d="M136 2.28882e-05H0L0.000144482 136H136V2.28882e-05ZM27.27 50.6401C27.27 43.2101 33.3 37.1801 40.73 37.1801H66.64C67.02 37.1801 67.37 37.4101 67.53 37.7601C67.69 38.1101 67.62 38.5201 67.36 38.8101L61.68 44.9801C60.69 46.0501 59.3 46.6701 57.84 46.6701H40.8C38.57 46.6701 36.76 48.4801 36.76 50.7101V60.8901C36.76 62.2001 35.7 63.2601 34.39 63.2601H29.65C28.34 63.2601 27.28 62.2001 27.28 60.8901V50.6401H27.27ZM107.88 85.3601C107.88 92.7901 101.85 98.82 94.42 98.82H83.41C75.98 98.82 69.95 92.7901 69.95 85.3601V66.0901C69.95 64.7801 70.44 63.5201 71.33 62.5501L77.75 55.5801C78.02 55.2901 78.44 55.1901 78.82 55.3301C79.19 55.4801 79.44 55.83 79.44 56.23V85.3001C79.44 87.5301 81.25 89.3401 83.48 89.3401H94.36C96.59 89.3401 98.4 87.5301 98.4 85.3001V50.7101C98.4 48.4801 96.59 46.6701 94.36 46.6701H81.71C80.26 46.6701 78.88 47.2801 77.89 48.3401L40.16 89.3401H62.83C64.14 89.3401 65.2 90.4001 65.2 91.7101V96.4501C65.2 97.7601 64.14 98.82 62.83 98.82H32.28C29.51 98.82 27.26 96.5701 27.26 93.8001V91.29C27.26 90.03 27.73 88.8201 28.59 87.8901L70.89 41.9401C73.69 38.9001 77.62 37.1801 81.75 37.1801H94.41C101.84 37.1801 107.87 43.2101 107.87 50.6401V85.3601H107.88Z"
fill="black"
/>
<path
d="M27.27 50.6401C27.27 43.2101 33.3 37.1801 40.73 37.1801H66.64C67.02 37.1801 67.37 37.4101 67.53 37.7601C67.69 38.1101 67.62 38.5201 67.36 38.8101L61.68 44.9801C60.69 46.0501 59.3 46.6701 57.84 46.6701H40.8C38.57 46.6701 36.76 48.4801 36.76 50.7101V60.8901C36.76 62.2001 35.7 63.2601 34.39 63.2601H29.65C28.34 63.2601 27.28 62.2001 27.28 60.8901V50.6401H27.27Z"
fill="white"
/>
<path
d="M107.88 85.3601C107.88 92.7901 101.85 98.82 94.42 98.82H83.41C75.98 98.82 69.95 92.7901 69.95 85.3601V66.0901C69.95 64.7801 70.44 63.5201 71.33 62.5501L77.75 55.5801C78.02 55.2901 78.44 55.1901 78.82 55.3301C79.19 55.4801 79.44 55.83 79.44 56.23V85.3001C79.44 87.5301 81.25 89.3401 83.48 89.3401H94.36C96.59 89.3401 98.4 87.5301 98.4 85.3001V50.7101C98.4 48.4801 96.59 46.6701 94.36 46.6701H81.71C80.26 46.6701 78.88 47.2801 77.89 48.3401L40.16 89.3401H62.83C64.14 89.3401 65.2 90.4001 65.2 91.7101V96.4501C65.2 97.7601 64.14 98.82 62.83 98.82H32.28C29.51 98.82 27.26 96.5701 27.26 93.8001V91.29C27.26 90.03 27.73 88.8201 28.59 87.8901L70.89 41.9401C73.69 38.9001 77.62 37.1801 81.75 37.1801H94.41C101.84 37.1801 107.87 43.2101 107.87 50.6401V85.3601H107.88Z"
fill="white"
/>
</g>
<defs>
<clipPath id="clip0_2343_96406">
<rect width="136" height="136" rx="16" fill="white" />
</clipPath>
</defs>
</svg>
</div>
<div style={contributorInfoContainer}>
<div style={contributorInfoBox}>
<div style={contributorInfo}>
<h3 style={contributorInfoTitle}>Merged PR</h3>
<p style={contributorInfoStats}>{mergedPRsCount}</p>
</div>
<div style={infoSeparator} />
</div>
<div style={contributorInfoBox}>
<div style={contributorInfo}>
<h3 style={contributorInfoTitle}>Ranking</h3>
<p style={contributorInfoStats}>{rank}%</p>
</div>
<div style={infoSeparator} />
</div>
<div style={contributorInfoBox}>
<div style={contributorInfo}>
<h3 style={contributorInfoTitle}>Active Days</h3>
<h1 style={contributorInfoStats}>{activeDays}</h1>
</div>
</div>
</div>
</div>
),
{
width: 1200,
height: 630,
fonts: [
{
name: 'Gabarito',
data: await getGabarito(),
style: 'normal',
},
],
},
);
}
} catch (error) {
return new Response(`error: ${error}`, {
status: 500,
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { CSSProperties } from 'react';

const BACKGROUND_IMAGE_URL =
'https://framerusercontent.com/images/nqEmdwe7yDXNsOZovuxG5zvj2E.png';

export const container: CSSProperties = {
display: 'flex',
flexDirection: 'column',
width: '1200px',
height: '630px',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'white',
fontFamily: 'Gabarito',
};

export const topBackgroundImage: CSSProperties = {
backgroundImage: `url(${BACKGROUND_IMAGE_URL})`,
position: 'absolute',
zIndex: '-1',
width: '1300px',
height: '250px',
transform: 'rotate(-11deg)',
opacity: '0.2',
top: '-100',
left: '-25',
};

export const bottomBackgroundImage: CSSProperties = {
backgroundImage: `url(${BACKGROUND_IMAGE_URL})`,
position: 'absolute',
zIndex: '-1',
width: '1300px',
height: '250px',
transform: 'rotate(-11deg)',
opacity: '0.2',
bottom: '-120',
right: '-40',
};

export const profileContainer: CSSProperties = {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
width: '780px',
margin: '0px 0px 40px',
};

export const styledContributorAvatar = {
display: 'flex',
width: '96px',
height: '96px',
margin: '0px',
border: '3px solid #141414',
borderRadius: '16px',
};

export const profileInfoContainer: CSSProperties = {
display: 'flex',
flexDirection: 'column',
gap: '8px',
alignItems: 'center',
justifyContent: 'center',
};

export const profileUsernameHeader: CSSProperties = {
margin: '0px',
fontSize: '28px',
fontWeight: '700',
color: '#141414',
fontFamily: 'Gabarito',
};

export const profileContributionHeader: CSSProperties = {
margin: '0px',
color: '#818181',
fontSize: '20px',
fontWeight: '400',
};

export const contributorInfoContainer: CSSProperties = {
border: '3px solid #141414',
borderRadius: '12px',
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-around',
width: '780px',
height: '149px',
backgroundColor: '#F1F1F1',
};

export const contributorInfoBox: CSSProperties = {
flex: 1,
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
position: 'relative',
};

export const contributorInfo: CSSProperties = {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
margin: '32px',
gap: '16px',
};

export const contributorInfoTitle = {
color: '#B3B3B3',
margin: '0px',
fontWeight: '500',
fontSize: '24px',
};

export const contributorInfoStats = {
color: '#474747',
margin: '0px',
fontWeight: '700',
fontSize: '40px',
};

export const infoSeparator: CSSProperties = {
position: 'absolute',
right: 0,
display: 'flex',
width: '2px',
height: '85px',
backgroundColor: '#141414',
};
Loading

0 comments on commit 2067069

Please sign in to comment.