GH 3365 Add contributors page on twenty-website (#3745)

* add transition in mobile navbar

* add contributors listing page

* Add breadcrumb component

* Add profilecard component

* Make profile info dynamic

* Style activity log component

* Make title a re-usable component

* Make card container re-usable

* add rank and active days logic

* complete single contributor page

* add styles for mobile

* Add github link

* Reset header desktop

* update calendar height

* remove conditional header

* add GH PR link

* display 10 prs

* Remove employees and fix rank

* Unrelated CSS adjustment

---------

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
Deepak Kumar
2024-02-05 18:26:12 +05:30
committed by GitHub
parent 33bb48e681
commit 8dda4b0b8f
20 changed files with 749 additions and 127 deletions

View File

@ -9,14 +9,29 @@ export interface User {
}
const AvatarGridContainer = styled.div`
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
grid-gap: 10px;
margin: 0 auto;
max-width: 1024px;
justify-items: center;
display: flex;
justify-content: center;
gap: 32px;
flex-wrap: wrap;
`;
const AvatarItem = styled.div`
position: relative;
width: 100%;
width: 124px;
height: 124px;
border: 3px solid #141414;
border-radius: 16px;
overflow: hidden;
transition: 200ms;
&:hover {
-webkit-box-shadow: -6px 6px 0px 1px rgba(0, 0, 0, 1);
-moz-box-shadow: -6px 6px 0px 1px rgba(0, 0, 0, 1);
box-shadow: -6px 6px 0px 1px rgba(0, 0, 0, 1);
}
img {
width: 100%;

View File

@ -0,0 +1,55 @@
import React from 'react';
import styled from '@emotion/styled';
import Link from 'next/link';
const Container = styled.div`
display: flex;
gap: 8px;
color: #b3b3b3;
`;
const InternalLinkItem = styled(Link)`
text-decoration: none;
color: #b3b3b3;
`;
const ExternalLinkItem = styled.a`
text-decoration: none;
color: #b3b3b3;
`;
const ActivePage = styled.span`
color: #818181;
`;
interface BreadcrumbsProps {
items: {
uri: string;
label: string;
isExternal?: boolean;
}[];
activePage: string;
separator: string;
}
export const Breadcrumbs = ({
items,
activePage,
separator,
}: BreadcrumbsProps) => {
return (
<Container>
{items.map((item, index) => (
<React.Fragment key={`${item?.uri ?? 'item'}-${index}`}>
{item.isExternal ? (
<ExternalLinkItem href={item.uri}>{item.label}</ExternalLinkItem>
) : (
<InternalLinkItem href={item.uri}>{item.label}</InternalLinkItem>
)}
<span>{separator}</span>
</React.Fragment>
))}
<ActivePage>{activePage}</ActivePage>
</Container>
);
};

View File

@ -2,11 +2,10 @@
import styled from '@emotion/styled';
import { IBM_Plex_Mono } from 'next/font/google';
import { usePathname } from 'next/navigation';
import { ExternalArrow } from '@/app/components/ExternalArrow';
import { DiscordIcon, GithubIcon, GithubIcon2, XIcon } from './Icons';
import { GithubIcon } from './Icons';
import { Logo } from './Logo';
const IBMPlexMono = IBM_Plex_Mono({
@ -102,88 +101,34 @@ const LinkNextToCTA = styled.a`
`;
const CallToAction = () => {
const path = usePathname();
const isTwentyDev = path.includes('developers');
return (
<CallToActionContainer>
{isTwentyDev ? (
<>
<div
style={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
gap: '10px',
}}
>
<a href="https://x.com/twentycrm" target="_blank" rel="noreferrer">
<XIcon size="M" />
</a>
<a
href="https://github.com/twentyhq/twenty"
target="_blank"
rel="noreferrer"
>
<GithubIcon2 size="M" />
</a>
<a
href="https://discord.gg/UfGNZJfAG6"
target="_blank"
rel="noreferrer"
>
<DiscordIcon size="M" />
</a>
</div>
</>
) : (
<>
<LinkNextToCTA href="https://github.com/twentyhq/twenty">
Sign in
</LinkNextToCTA>
<a href="https://twenty.com/stripe-redirection">
<StyledButton>Get Started</StyledButton>
</a>
</>
)}
<LinkNextToCTA href="https://github.com/twentyhq/twenty">
Sign in
</LinkNextToCTA>
<a href="https://twenty.com/stripe-redirection">
<StyledButton>Get Started</StyledButton>
</a>
</CallToActionContainer>
);
};
export const HeaderDesktop = () => {
const path = usePathname();
const isTwentyDev = path.includes('developers');
return (
<Nav>
<LogoContainer>
<Logo />
{isTwentyDev && (
<LogoAddon className={IBMPlexMono.className}>
for Developers
</LogoAddon>
)}
</LogoContainer>
{isTwentyDev ? (
<LinkList>
<ListItem href="/developers/docs">Docs</ListItem>
<ListItem href="/developers/contributors">Contributors</ListItem>
<ListItem href="/">
Cloud <ExternalArrow />
</ListItem>
</LinkList>
) : (
<LinkList>
<ListItem href="/pricing">Pricing</ListItem>
<ListItem href="/story">Story</ListItem>
<ListItem href="https://docs.twenty.com">
Docs <ExternalArrow />
</ListItem>
<ListItem href="https://github.com/twentyhq/twenty">
<GithubIcon color="rgb(71,71,71)" /> 5.7k <ExternalArrow />
</ListItem>
</LinkList>
)}
<LinkList>
<ListItem href="/pricing">Pricing</ListItem>
<ListItem href="/story">Story</ListItem>
<ListItem href="https://docs.twenty.com">
Docs <ExternalArrow />
</ListItem>
<ListItem href="https://github.com/twentyhq/twenty">
<GithubIcon color="rgb(71,71,71)" /> 8.3k <ExternalArrow />
</ListItem>
</LinkList>
<CallToAction />
</Nav>
);

View File

@ -36,6 +36,7 @@ const LinkList = styled.div`
display: flex;
flex-direction: column;
gap: 16px;
align-items: center;
`;
const ListItem = styled.a`
@ -45,7 +46,7 @@ const ListItem = styled.a`
gap: 4px;
align-items: center;
border-radius: 8px;
height: 40px;
height: 48px;
padding-left: 16px;
padding-right: 16px;
&:hover {
@ -174,6 +175,9 @@ const NavOpen = styled.div`
gap: 33px;
padding-top: 32px;
z-index: 100;
transition: transform 0.2s ease-in;
display: flex;
transform-origin: top;
`;
const MobileMenu = styled.div`
@ -208,15 +212,19 @@ export const HeaderMobile = () => {
<HamburgerLine2 id="line2" />
</HamburgerContainer>
</Nav>
<NavOpen style={{ display: menuOpen ? 'flex' : 'none' }}>
<NavOpen
style={{
transform: `scaleY(${menuOpen ? '1' : '0'})`,
}}
>
<LinkList>
<ListItem href="/pricing">Pricing</ListItem>
<ListItem href="/story">Story</ListItem>
<ListItem href="/pricing">Pricing</ListItem>
<ListItem href="https://docs.twenty.com">
Docs <ExternalArrow />
</ListItem>
<ListItem href="https://github.com/twentyhq/twenty">
<GithubIcon color="rgb(71,71,71)" /> 5.7k <ExternalArrow />
<GithubIcon color="rgb(71,71,71)" /> 8.3k <ExternalArrow />
</ListItem>
</LinkList>
<CallToAction />

View File

@ -7,7 +7,7 @@ const getSize = (size: string) => {
case 'L':
return '48px';
default:
return '14px';
return size;
}
};
@ -117,3 +117,38 @@ export const GithubIcon2 = ({ size = 'S', color = 'rgb(179, 179, 179)' }) => {
</div>
);
};
export const PullRequestIcon = ({ size = 'S', color = 'rgb(179,179,179)' }) => {
const dimension = getSize(size);
return (
<div style={{ width: dimension, height: dimension }}>
<svg viewBox="0 0 18 19" xmlns="http://www.w3.org/2000/svg">
<path
d="M3 14C3.53043 14 4.03914 14.2107 4.41421 14.5858C4.78929 14.9609 5 15.4696 5 16C5 16.5304 4.78929 17.0391 4.41421 17.4142C4.03914 17.7893 3.53043 18 3 18C2.46957 18 1.96086 17.7893 1.58579 17.4142C1.21071 17.0391 1 16.5304 1 16C1 15.4696 1.21071 14.9609 1.58579 14.5858C1.96086 14.2107 2.46957 14 3 14ZM3 14V6M3 6C2.46957 6 1.96086 5.78929 1.58579 5.41421C1.21071 5.03914 1 4.53043 1 4C1 3.46957 1.21071 2.96086 1.58579 2.58579C1.96086 2.21071 2.46957 2 3 2C3.53043 2 4.03914 2.21071 4.41421 2.58579C4.78929 2.96086 5 3.46957 5 4C5 4.53043 4.78929 5.03914 4.41421 5.41421C4.03914 5.78929 3.53043 6 3 6ZM15 14C15.5304 14 16.0391 14.2107 16.4142 14.5858C16.7893 14.9609 17 15.4696 17 16C17 16.5304 16.7893 17.0391 16.4142 17.4142C16.0391 17.7893 15.5304 18 15 18C14.4696 18 13.9609 17.7893 13.5858 17.4142C13.2107 17.0391 13 16.5304 13 16C13 15.4696 13.2107 14.9609 13.5858 14.5858C13.9609 14.2107 14.4696 14 15 14ZM15 14V6C15 5.46957 14.7893 4.96086 14.4142 4.58579C14.0391 4.21071 13.5304 4 13 4H8M8 4L11 7M8 4L11 1"
stroke={color}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
fill="transparent"
/>
</svg>
</div>
);
};
export const HeartIcon = ({ size = 'S', color = 'rgb(179,179,179)' }) => {
const dimension = getSize(size);
return (
<div style={{ width: dimension, height: dimension }}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 20" fill="none">
<path
d="M18.513 10.572L11.013 18L3.513 10.572C3.0183 10.0906 2.62864 9.51201 2.36854 8.87263C2.10845 8.23325 1.98356 7.54694 2.00173 6.85693C2.01991 6.16691 2.18076 5.48813 2.47415 4.86333C2.76755 4.23853 3.18713 3.68125 3.70648 3.22657C4.22583 2.7719 4.8337 2.42968 5.49181 2.22147C6.14991 2.01327 6.844 1.94358 7.53036 2.0168C8.21673 2.09001 8.8805 2.30455 9.47987 2.6469C10.0792 2.98925 10.6012 3.45199 11.013 4.00599C11.4265 3.45602 11.9491 2.99731 12.5481 2.6586C13.1471 2.31988 13.8095 2.10844 14.4939 2.03751C15.1784 1.96658 15.8701 2.03769 16.5258 2.24639C17.1815 2.45508 17.787 2.79687 18.3045 3.25036C18.8221 3.70385 19.2404 4.25928 19.5334 4.88189C19.8264 5.50449 19.9877 6.18088 20.0073 6.8687C20.0269 7.55653 19.9043 8.24099 19.6471 8.87924C19.39 9.5175 19.0039 10.0958 18.513 10.578"
stroke={color}
strokeWidth="2.6"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
);
};

View File

@ -0,0 +1,25 @@
'use client';
import styled from '@emotion/styled';
const Title = styled.h2`
font-size: 56px;
font-weight: 600;
color: #b3b3b3;
margin-bottom: 0px;
margin-top: 64px;
@media (max-width: 810px) {
font-size: 28px;
}
`;
export const Header = () => {
return (
<>
<Title>
Our amazing <br /> <span style={{ color: 'black' }}>Contributors</span>
</Title>
</>
);
};

View File

@ -7,19 +7,20 @@ const BackgroundContainer = styled.div`
top: 100%;
left: 0px;
width: 100%;
height: 200%;
height: 100%;
max-height: 200%;
background-image: url(https://framerusercontent.com/images/nqEmdwe7yDXNsOZovuxG5zvj2E.png);
background-size: auto 20px;
background-repeat: repeat;
transform-origin: center center;
z-index: -2;
}
`;
const Gradient = styled.div`
position: absolute;
width: 100%;
height: 200%;
height: 100%;
max-height: 200%;
top: 100%;
left: 0;
right: 0;

View File

@ -2,10 +2,28 @@
import { ResponsiveTimeRange } from '@nivo/calendar';
import { CardContainer } from '@/app/developers/contributors/[slug]/components/CardContainer';
import { Title } from '@/app/developers/contributors/[slug]/components/Title';
export const ActivityLog = ({
data,
}: {
data: { value: number; day: string }[];
}) => {
return <ResponsiveTimeRange data={data} />;
return (
<CardContainer>
<Title>Activity</Title>
<div style={{ width: '100%', height: '214px' }}>
<ResponsiveTimeRange
data={data}
emptyColor="#F4EFFF"
colors={['#E9DFFF', '#B28FFE', '#915FFD']}
dayBorderWidth={2}
dayBorderColor="#ffffff"
dayRadius={4}
daySpacing={2}
/>
</div>
</CardContainer>
);
};

View File

@ -0,0 +1,16 @@
'use client';
import { Breadcrumbs } from '@/app/components/Breadcrumbs';
const BREADCRUMB_ITEMS = [
{
uri: '/developers/contributors',
label: 'Contributors',
},
];
export const Breadcrumb = ({ active }: { active: string }) => {
return (
<Breadcrumbs items={BREADCRUMB_ITEMS} activePage={active} separator="/" />
);
};

View File

@ -0,0 +1,17 @@
import styled from '@emotion/styled';
export const CardContainer = styled.div`
border: 3px solid #141414;
width: 100%;
border-radius: 12px;
padding: 40px;
display: flex;
gap: 32px;
flex-direction: column;
background-color: #fafafa;
@media (max-width: 810px) {
gap: 16px;
padding: 24px;
}
`;

View File

@ -0,0 +1,26 @@
'use client';
import styled from '@emotion/styled';
const Container = styled.div`
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
max-width: 898px;
padding: 40px;
gap: 40px;
@media (max-width: 809px) {
width: 100%;
padding: 40px 24px 40px 24px;
}
`;
export const ContentContainer = ({
children,
}: {
children?: React.ReactNode;
}) => {
return <Container>{children}</Container>;
};

View File

@ -0,0 +1,100 @@
'use client';
import styled from '@emotion/styled';
import { format } from 'date-fns';
import { GithubIcon } from '@/app/components/Icons';
const ProfileContainer = styled.div`
display: flex;
gap: 32px;
width: 100%;
align-items: center;
`;
const Avatar = styled.div`
border: 3px solid #141414;
width: 96px;
height: 96px;
border-radius: 16px;
overflow: hidden;
flex-shrink: 0;
img {
width: 100%;
display: block;
height: 100%;
object-fit: cover;
}
`;
const Details = styled.div`
display: flex;
flex-direction: column;
gap: 4px;
.username {
font-size: 40px;
font-weight: 700;
line-height: 48px;
margin: 0;
display: flex;
gap: 12px;
@media (max-width: 810px) {
font-size: 24px;
line-height: 28.8px;
}
}
.duration {
font-size: 24px;
font-weight: 400;
color: #818181;
margin: 0;
@media (max-width: 810px) {
font-size: 20px;
line-height: 28px;
}
}
`;
const StyledGithubIcon = styled(GithubIcon)`
@media (max-width: 810px) {
width: 20px;
height: 20px;
}
`;
interface ProfileCardProps {
username: string;
avatarUrl: string;
firstContributionAt: string;
}
export const ProfileCard = ({
username,
avatarUrl,
firstContributionAt,
}: ProfileCardProps) => {
return (
<ProfileContainer>
<Avatar>
<img src={avatarUrl} alt={username} />
</Avatar>
<Details>
<h3 className="username">
@{username}
<a href={`https://github.com/${username}`} target="_blank">
<StyledGithubIcon size="M" color="rgba(0,0,0,1)" />
</a>
</h3>
<p className="duration">
Contributing since{' '}
{format(new Date(firstContributionAt), 'MMMM yyyy')}
</p>
</Details>
</ProfileContainer>
);
};

View File

@ -0,0 +1,86 @@
'use client';
import styled from '@emotion/styled';
import { CardContainer } from '@/app/developers/contributors/[slug]/components/CardContainer';
const Container = styled(CardContainer)`
flex-direction: row;
@media (max-width: 810px) {
flex-direction: column;
}
.item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex-grow: 1;
flex-basis: 0;
.title {
font-size: 24px;
color: #b3b3b3;
margin: 0;
@media (max-width: 810px) {
font-size: 20px;
}
}
.value {
font-size: 56px;
font-weight: 700;
color: #474747;
@media (max-width: 810px) {
font-size: 32px;
}
}
}
.separator {
width: 2px;
background-color: #141414;
border-radius: 40px;
@media (max-width: 810px) {
width: 100%;
height: 2px;
}
}
`;
interface ProfileInfoProps {
mergedPRsCount: number;
rank: string;
activeDays: number;
}
export const ProfileInfo = ({
mergedPRsCount,
rank,
activeDays,
}: ProfileInfoProps) => {
return (
<>
<Container>
<div className="item">
<p className="title">Merged PR</p>
<span className="value">{mergedPRsCount}</span>
</div>
<div className="separator"></div>
<div className="item">
<p className="title">Rank</p>
<span className="value">{rank}%</span>
</div>
<div className="separator"></div>
<div className="item">
<p className="title">Active Days</p>
<span className="value">{activeDays}</span>
</div>
</Container>
</>
);
};

View File

@ -0,0 +1,94 @@
import { Tooltip } from 'react-tooltip';
import styled from '@emotion/styled';
import { format } from 'date-fns';
import { PullRequestIcon } from '@/app/components/Icons';
import { formatIntoRelativeDate } from '@/lib/utils';
const StyledTooltip = styled(Tooltip)``;
const Item = styled.div`
display: flex;
gap: 17px;
`;
const StyledTitle = styled.a`
font-size: 24px;
font-weight: 400;
color: #474747;
margin: 0;
text-decoration: none;
@media (max-width: 810px) {
font-size: 20px;
}
`;
const StyledDescription = styled.div`
font-size: 20px;
line-height: 28px;
font-weight: 500;
color: #b3b3b3;
@media (max-width: 810px) {
font-size: 18px;
}
`;
const StyledPullRequestIcon = styled(PullRequestIcon)`
@media screen {
width: 20px;
height: 20px;
}
`;
interface PullRequestItemProps {
id: string;
title: string;
url: string;
createdAt: string;
mergedAt: string | null;
authorId: string;
}
export const PullRequestItem = ({
id,
title,
url,
createdAt,
mergedAt,
authorId,
}: PullRequestItemProps) => {
const prNumber = url.split('/').slice(-1)[0];
return (
<Item key={id}>
<div>
<StyledPullRequestIcon
color={mergedAt ? '#915FFD' : '#1A7F37'}
size="M"
/>
</div>
<div>
<StyledTitle href={url} target="_blank">
{title}
</StyledTitle>
<StyledDescription>
#{prNumber} by {authorId.slice(1)} was{' '}
{mergedAt ? `merged` : `opened`}{' '}
<span id={`date-${prNumber}`}>
{formatIntoRelativeDate(mergedAt ? mergedAt : createdAt)}
</span>
<StyledTooltip
anchorSelect={`#date-${prNumber}`}
content={format(
new Date(mergedAt ? mergedAt : createdAt),
'dd MMMM yyyy',
)}
clickable
noArrow
/>
</StyledDescription>
</div>
</Item>
);
};

View File

@ -0,0 +1,44 @@
'use client';
import styled from '@emotion/styled';
import { CardContainer } from '@/app/developers/contributors/[slug]/components/CardContainer';
import { PullRequestItem } from '@/app/developers/contributors/[slug]/components/PullRequestItem';
import { Title } from '@/app/developers/contributors/[slug]/components/Title';
const List = styled.div`
display: flex;
gap: 24px;
flex-direction: column;
`;
interface PullRequestsProps {
list: {
id: string;
title: string;
url: string;
createdAt: string;
mergedAt: string | null;
authorId: string;
}[];
}
export const PullRequests = ({ list }: PullRequestsProps) => {
return (
<CardContainer>
<Title>Pull Requests</Title>
<List>
{list.map((pr) => (
<PullRequestItem
key={pr.id}
id={pr.id}
title={pr.title}
url={pr.url}
createdAt={pr.createdAt}
mergedAt={pr.mergedAt}
authorId={pr.authorId}
/>
))}
</List>
</CardContainer>
);
};

View File

@ -0,0 +1,38 @@
'use client';
import styled from '@emotion/styled';
import { HeartIcon } from '@/app/components/Icons';
import { CardContainer } from '@/app/developers/contributors/[slug]/components/CardContainer';
const StyledTitle = styled.div`
display: flex;
font-size: 24px;
font-weight: 500;
gap: 8px;
@media (max-width: 810px) {
font-size: 20px;
}
`;
const StyledHeartIcon = styled(HeartIcon)`
@media (max-width: 810px) {
width: 16px !important;
height: 16px !important;
}
`;
interface ThankYouProps {
authorId: string;
}
export const ThankYou = ({ authorId }: ThankYouProps) => {
return (
<CardContainer>
<StyledTitle>
Thank you @{authorId} <StyledHeartIcon color="#333333" size="18px" />
</StyledTitle>
</CardContainer>
);
};

View File

@ -0,0 +1,13 @@
import styled from '@emotion/styled';
export const Title = styled.h3`
margin: 0;
font-size: 32px;
line-height: 41.6px;
font-weight: 500;
@media (max-width: 810px) {
font-size: 24px;
line-height: 31.2px;
}
`;

View File

@ -1,8 +1,14 @@
import Database from 'better-sqlite3';
import { Metadata } from 'next';
import Image from 'next/image';
import { ActivityLog } from './components/ActivityLog';
import { Background } from '@/app/components/oss-friends/Background';
import { ActivityLog } from '@/app/developers/contributors/[slug]/components/ActivityLog';
import { Breadcrumb } from '@/app/developers/contributors/[slug]/components/Breadcrumb';
import { ContentContainer } from '@/app/developers/contributors/[slug]/components/ContentContainer';
import { ProfileCard } from '@/app/developers/contributors/[slug]/components/ProfileCard';
import { ProfileInfo } from '@/app/developers/contributors/[slug]/components/ProfileInfo';
import { PullRequests } from '@/app/developers/contributors/[slug]/components/PullRequests';
import { ThankYou } from '@/app/developers/contributors/[slug]/components/ThankYou';
interface Contributor {
login: string;
@ -57,6 +63,7 @@ export default async function ({ params }: { params: { slug: string } }) {
)
.all({ user_id: params.slug }) as { value: number; day: string }[];
// Latest PRs.
const pullRequestList = db
.prepare(
`
@ -68,53 +75,88 @@ export default async function ({ params }: { params: { slug: string } }) {
createdAt,
updatedAt,
closedAt,
mergedAt
mergedAt,
authorId
FROM
pullRequests
WHERE
authorId = (SELECT id FROM users WHERE login = :user_id)
ORDER BY
DATE(createdAt) DESC
LIMIT
10
`,
)
.all({ user_id: params.slug }) as {
title: string;
createdAt: string;
url: string;
id: string;
mergedAt: string | null;
authorId: string;
}[];
const mergedPullRequests = db
.prepare(
`
SELECT * FROM (
SELECT
merged_pr_counts.*,
(RANK() OVER(ORDER BY merged_count) - 1) / CAST( total_authors as float) * 100 as rank_percentage
FROM
(
SELECT
authorId,
COUNT(*) FILTER (WHERE mergedAt IS NOT NULL) as merged_count
FROM
pullRequests pr
JOIN
users u ON pr.authorId = u.id
WHERE
u.isEmployee = FALSE
GROUP BY
authorId
) AS merged_pr_counts
CROSS JOIN
(
SELECT COUNT(DISTINCT authorId) as total_authors
FROM pullRequests pr
JOIN
users u ON pr.authorId = u.id
WHERE
u.isEmployee = FALSE
) AS author_counts
) WHERE authorId = (SELECT id FROM users WHERE login = :user_id)
`,
)
.all({ user_id: params.slug }) as {
merged_count: number;
rank_percentage: number;
}[];
db.close();
return (
<div
style={{
maxWidth: '900px',
display: 'flex',
padding: '40px',
gap: '24px',
}}
>
<div style={{ flexDirection: 'column', width: '240px' }}>
<Image
src={contributor.avatarUrl}
alt={contributor.login}
width={240}
height={240}
<>
<Background />
<ContentContainer>
<Breadcrumb active={contributor.login} />
<ProfileCard
username={contributor.login}
avatarUrl={contributor.avatarUrl}
firstContributionAt={pullRequestActivity[0].day}
/>
<h1>{contributor.login}</h1>
</div>
<div style={{ flexDirection: 'column' }}>
<div style={{ width: '450px', height: '200px' }}>
<ActivityLog data={pullRequestActivity} />
</div>
<div style={{ width: '450px' }}>
{pullRequestList.map((pr) => (
<div>
<a href={pr.url}>{pr.title}</a>
</div>
))}
</div>
</div>
</div>
<ProfileInfo
mergedPRsCount={mergedPullRequests[0].merged_count}
rank={(100 - Number(mergedPullRequests[0].rank_percentage)).toFixed(
0,
)}
activeDays={pullRequestActivity.length}
/>
<ActivityLog data={pullRequestActivity} />
<PullRequests list={pullRequestList} />
<ThankYou authorId={contributor.login} />
</ContentContainer>
</>
);
}

View File

@ -1,6 +1,9 @@
import Database from 'better-sqlite3';
import AvatarGrid from '@/app/components/AvatarGrid';
import { Header } from '@/app/components/developers/contributors/Header';
import { Background } from '@/app/components/oss-friends/Background';
import { ContentContainer } from '@/app/components/oss-friends/ContentContainer';
interface Contributor {
login: string;
@ -14,16 +17,19 @@ const Contributors = async () => {
const contributors = db
.prepare(
`SELECT
u.login,
u.avatarUrl,
COUNT(pr.id) AS pullRequestCount
FROM
u.login,
u.avatarUrl,
COUNT(pr.id) AS pullRequestCount
FROM
users u
JOIN
JOIN
pullRequests pr ON u.id = pr.authorId
GROUP BY
WHERE
u.isEmployee = FALSE
AND u.login NOT IN ('dependabot', 'cyborch', 'emilienchvt', 'Samox')
GROUP BY
u.id
ORDER BY
ORDER BY
pullRequestCount DESC;
`,
)
@ -32,9 +38,15 @@ const Contributors = async () => {
db.close();
return (
<div>
<AvatarGrid users={contributors} />
</div>
<>
<Background />
<ContentContainer>
<Header />
<div>
<AvatarGrid users={contributors} />
</div>
</ContentContainer>
</>
);
};

View File

@ -0,0 +1,32 @@
import { differenceInDays, formatDistance } from 'date-fns';
const formatIntoRelativeDate = (dateString: string) => {
if (!dateString) return '';
const inputDate = new Date(dateString);
const currentDate = new Date();
const daysDifference = differenceInDays(currentDate, inputDate);
let formattedDate = '';
if (daysDifference === 0) {
formattedDate = 'today';
} else if (daysDifference === 1) {
formattedDate = 'yesterday';
} else if (daysDifference < 7) {
formattedDate = formatDistance(inputDate, currentDate, { addSuffix: true });
} else if (daysDifference < 14) {
formattedDate = 'last week';
} else if (daysDifference < 30) {
formattedDate = Math.floor(daysDifference / 7) + ' weeks ago';
} else if (daysDifference < 60) {
formattedDate = 'last month';
} else if (daysDifference < 365) {
formattedDate = Math.floor(daysDifference / 30) + ' months';
} else if (daysDifference < 730) {
formattedDate = 'last year';
} else {
formattedDate = Math.floor(daysDifference / 365) + ' years ago';
}
return formattedDate;
};
export { formatIntoRelativeDate };