@ -1,32 +0,0 @@
|
||||
'use client';
|
||||
|
||||
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 }[];
|
||||
}) => {
|
||||
if (!data.length) {
|
||||
return null;
|
||||
}
|
||||
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>
|
||||
);
|
||||
};
|
||||
@ -1,16 +0,0 @@
|
||||
'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="/" />
|
||||
);
|
||||
};
|
||||
@ -1,17 +0,0 @@
|
||||
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;
|
||||
}
|
||||
`;
|
||||
@ -1,26 +0,0 @@
|
||||
'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>;
|
||||
};
|
||||
@ -1,102 +0,0 @@
|
||||
'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>
|
||||
{firstContributionAt && (
|
||||
<p className="duration">
|
||||
Contributing since{' '}
|
||||
{format(new Date(firstContributionAt), 'MMMM yyyy')}
|
||||
</p>
|
||||
)}
|
||||
</Details>
|
||||
</ProfileContainer>
|
||||
);
|
||||
};
|
||||
@ -1,86 +0,0 @@
|
||||
'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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,109 +0,0 @@
|
||||
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 StyledPrLink = styled.a`
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
color: #474747;
|
||||
}
|
||||
`;
|
||||
|
||||
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>
|
||||
<StyledPrLink
|
||||
href={'https://github.com/twentyhq/twenty/pull/' + prNumber}
|
||||
target="__blank"
|
||||
>
|
||||
#{prNumber}
|
||||
</StyledPrLink>{' '}
|
||||
by {authorId} 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>
|
||||
);
|
||||
};
|
||||
@ -1,44 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
};
|
||||
@ -1,38 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
};
|
||||
@ -1,13 +0,0 @@
|
||||
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;
|
||||
}
|
||||
`;
|
||||
@ -1,134 +0,0 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
import { Metadata } from 'next';
|
||||
|
||||
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';
|
||||
import { findAll } from '@/database/database';
|
||||
import { pullRequestModel, userModel } from '@/database/model';
|
||||
|
||||
export function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: { slug: string };
|
||||
}): Metadata {
|
||||
return {
|
||||
title: params.slug + ' | Contributors',
|
||||
};
|
||||
}
|
||||
|
||||
export default async function ({ params }: { params: { slug: string } }) {
|
||||
const contributors = await findAll(userModel);
|
||||
|
||||
const contributor = contributors.find(
|
||||
(contributor) => contributor.id === params.slug,
|
||||
);
|
||||
|
||||
if (!contributor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pullRequests = await findAll(pullRequestModel);
|
||||
const mergedPullRequests = pullRequests
|
||||
.filter((pr) => pr.mergedAt !== null)
|
||||
.filter(
|
||||
(pr) =>
|
||||
![
|
||||
'dependabot',
|
||||
'cyborch',
|
||||
'emilienchvt',
|
||||
'Samox',
|
||||
'charlesBochet',
|
||||
'gitstart-app',
|
||||
'thaisguigon',
|
||||
'lucasbordeau',
|
||||
'magrinj',
|
||||
'Weiko',
|
||||
'gitstart-twenty',
|
||||
'bosiraphael',
|
||||
'martmull',
|
||||
'FelixMalfait',
|
||||
'thomtrp',
|
||||
'Bonapara',
|
||||
'nimraahmed',
|
||||
].includes(pr.authorId),
|
||||
);
|
||||
|
||||
const contributorPullRequests = pullRequests.filter(
|
||||
(pr) => pr.authorId === contributor.id,
|
||||
);
|
||||
const mergedContributorPullRequests = contributorPullRequests.filter(
|
||||
(pr) => pr.mergedAt !== null,
|
||||
);
|
||||
|
||||
const mergedContributorPullRequestsByContributor = mergedPullRequests.reduce(
|
||||
(acc, pr) => {
|
||||
acc[pr.authorId] = (acc[pr.authorId] || 0) + 1;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const mergedContributorPullRequestsByContributorArray = Object.entries(
|
||||
mergedContributorPullRequestsByContributor,
|
||||
)
|
||||
.map(([authorId, value]) => ({ authorId, value }))
|
||||
.sort((a, b) => b.value - a.value);
|
||||
|
||||
const contributorRank =
|
||||
((mergedContributorPullRequestsByContributorArray.findIndex(
|
||||
(contributor) => contributor.authorId === params.slug,
|
||||
) +
|
||||
1) /
|
||||
contributors.length) *
|
||||
100;
|
||||
|
||||
const pullRequestActivity = contributorPullRequests.reduce((acc, pr) => {
|
||||
const date = new Date(pr.createdAt).toISOString().split('T')[0];
|
||||
acc[date] = (acc[date] || 0) + 1;
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const pullRequestActivityArray = Object.entries(pullRequestActivity)
|
||||
.map(([day, value]) => ({ day, value }))
|
||||
.sort((a, b) => new Date(a.day).getTime() - new Date(b.day).getTime());
|
||||
|
||||
return (
|
||||
<>
|
||||
<Background />
|
||||
<ContentContainer>
|
||||
<Breadcrumb active={contributor.id} />
|
||||
<ProfileCard
|
||||
username={contributor.id}
|
||||
avatarUrl={contributor.avatarUrl}
|
||||
firstContributionAt={pullRequestActivityArray[0]?.day}
|
||||
/>
|
||||
<ProfileInfo
|
||||
mergedPRsCount={mergedContributorPullRequests.length}
|
||||
rank={Math.ceil(Number(contributorRank)).toFixed(0)}
|
||||
activeDays={pullRequestActivityArray.length}
|
||||
/>
|
||||
<ActivityLog data={pullRequestActivityArray} />
|
||||
<PullRequests
|
||||
list={
|
||||
contributorPullRequests.slice(0, 9) as {
|
||||
id: string;
|
||||
title: string;
|
||||
url: string;
|
||||
createdAt: string;
|
||||
mergedAt: string | null;
|
||||
authorId: string;
|
||||
}[]
|
||||
}
|
||||
/>
|
||||
<ThankYou authorId={contributor.login} />
|
||||
</ContentContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,335 +0,0 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
import { global } from '@apollo/client/utilities/globals';
|
||||
import { graphql } from '@octokit/graphql';
|
||||
|
||||
import { insertMany, migrate } from '@/database/database';
|
||||
import {
|
||||
issueLabelModel,
|
||||
issueModel,
|
||||
labelModel,
|
||||
pullRequestLabelModel,
|
||||
pullRequestModel,
|
||||
userModel,
|
||||
} from '@/database/model';
|
||||
|
||||
interface LabelNode {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface AuthorNode {
|
||||
resourcePath: string;
|
||||
login: string;
|
||||
avatarUrl: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface PullRequestNode {
|
||||
id: string;
|
||||
title: string;
|
||||
body: string;
|
||||
url: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
closedAt: string;
|
||||
mergedAt: string;
|
||||
author: AuthorNode;
|
||||
labels: {
|
||||
nodes: LabelNode[];
|
||||
};
|
||||
}
|
||||
|
||||
interface IssueNode {
|
||||
id: string;
|
||||
title: string;
|
||||
body: string;
|
||||
url: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
closedAt: string;
|
||||
author: AuthorNode;
|
||||
labels: {
|
||||
nodes: LabelNode[];
|
||||
};
|
||||
}
|
||||
|
||||
interface PageInfo {
|
||||
hasNextPage: boolean;
|
||||
endCursor: string | null;
|
||||
}
|
||||
|
||||
interface PullRequests {
|
||||
nodes: PullRequestNode[];
|
||||
pageInfo: PageInfo;
|
||||
}
|
||||
|
||||
interface Issues {
|
||||
nodes: IssueNode[];
|
||||
pageInfo: PageInfo;
|
||||
}
|
||||
|
||||
interface AssignableUserNode {
|
||||
login: string;
|
||||
}
|
||||
|
||||
interface AssignableUsers {
|
||||
nodes: AssignableUserNode[];
|
||||
}
|
||||
|
||||
interface RepoData {
|
||||
repository: {
|
||||
pullRequests: PullRequests;
|
||||
issues: Issues;
|
||||
assignableUsers: AssignableUsers;
|
||||
};
|
||||
}
|
||||
|
||||
const query = graphql.defaults({
|
||||
headers: {
|
||||
Authorization: 'bearer ' + global.process.env.GITHUB_TOKEN,
|
||||
},
|
||||
});
|
||||
|
||||
async function fetchData(
|
||||
cursor: string | null = null,
|
||||
isIssues: boolean = false,
|
||||
accumulatedData: Array<PullRequestNode | IssueNode> = [],
|
||||
): Promise<Array<PullRequestNode | IssueNode>> {
|
||||
const { repository } = await query<RepoData>(
|
||||
`
|
||||
query ($cursor: String) {
|
||||
repository(owner: "twentyhq", name: "twenty") {
|
||||
pullRequests(first: 100, after: $cursor, orderBy: {field: CREATED_AT, direction: DESC}) @skip(if: ${isIssues}) {
|
||||
nodes {
|
||||
id
|
||||
title
|
||||
body
|
||||
url
|
||||
createdAt
|
||||
updatedAt
|
||||
closedAt
|
||||
mergedAt
|
||||
author {
|
||||
resourcePath
|
||||
login
|
||||
avatarUrl(size: 460)
|
||||
url
|
||||
}
|
||||
labels(first: 100) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
color
|
||||
description
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
issues(first: 100, after: $cursor, orderBy: {field: CREATED_AT, direction: DESC}) @skip(if: ${!isIssues}) {
|
||||
nodes {
|
||||
id
|
||||
title
|
||||
body
|
||||
url
|
||||
createdAt
|
||||
updatedAt
|
||||
closedAt
|
||||
author {
|
||||
resourcePath
|
||||
login
|
||||
avatarUrl
|
||||
url
|
||||
}
|
||||
labels(first: 100) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
color
|
||||
description
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ cursor },
|
||||
);
|
||||
|
||||
const newAccumulatedData: Array<PullRequestNode | IssueNode> = [
|
||||
...accumulatedData,
|
||||
...(isIssues ? repository.issues.nodes : repository.pullRequests.nodes),
|
||||
];
|
||||
const pageInfo = isIssues
|
||||
? repository.issues.pageInfo
|
||||
: repository.pullRequests.pageInfo;
|
||||
|
||||
if (pageInfo.hasNextPage) {
|
||||
return fetchData(pageInfo.endCursor, isIssues, newAccumulatedData);
|
||||
} else {
|
||||
return newAccumulatedData;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAssignableUsers(): Promise<Set<string>> {
|
||||
const { repository } = await query<RepoData>(`
|
||||
query {
|
||||
repository(owner: "twentyhq", name: "twenty") {
|
||||
assignableUsers(first: 100) {
|
||||
nodes {
|
||||
login
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
return new Set(repository.assignableUsers.nodes.map((user) => user.login));
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
if (!global.process.env.GITHUB_TOKEN) {
|
||||
return new Response('No GitHub token provided', { status: 500 });
|
||||
}
|
||||
|
||||
await migrate();
|
||||
|
||||
// TODO if we ever hit API Rate Limiting
|
||||
const lastPRCursor = null;
|
||||
const lastIssueCursor = null;
|
||||
|
||||
const assignableUsers = await fetchAssignableUsers();
|
||||
const fetchedPRs = (await fetchData(lastPRCursor)) as Array<PullRequestNode>;
|
||||
const fetchedIssues = (await fetchData(
|
||||
lastIssueCursor,
|
||||
true,
|
||||
)) as Array<IssueNode>;
|
||||
|
||||
for (const pr of fetchedPRs) {
|
||||
if (pr.author == null) {
|
||||
continue;
|
||||
}
|
||||
await insertMany(
|
||||
userModel,
|
||||
[
|
||||
{
|
||||
id: pr.author.login,
|
||||
avatarUrl: pr.author.avatarUrl,
|
||||
url: pr.author.url,
|
||||
isEmployee: assignableUsers.has(pr.author.login) ? '1' : '0',
|
||||
},
|
||||
],
|
||||
{ onConflictKey: 'id' },
|
||||
);
|
||||
|
||||
await insertMany(
|
||||
pullRequestModel,
|
||||
[
|
||||
{
|
||||
id: pr.id,
|
||||
title: pr.title,
|
||||
body: pr.body,
|
||||
url: pr.url,
|
||||
createdAt: pr.createdAt,
|
||||
updatedAt: pr.updatedAt,
|
||||
closedAt: pr.closedAt,
|
||||
mergedAt: pr.mergedAt,
|
||||
authorId: pr.author.login,
|
||||
},
|
||||
],
|
||||
{ onConflictKey: 'id' },
|
||||
);
|
||||
|
||||
for (const label of pr.labels.nodes) {
|
||||
await insertMany(
|
||||
labelModel,
|
||||
[
|
||||
{
|
||||
id: label.id,
|
||||
name: label.name,
|
||||
color: label.color,
|
||||
description: label.description,
|
||||
},
|
||||
],
|
||||
{ onConflictKey: 'id' },
|
||||
);
|
||||
await insertMany(pullRequestLabelModel, [
|
||||
{
|
||||
pullRequestId: pr.id,
|
||||
labelId: label.id,
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
for (const issue of fetchedIssues) {
|
||||
if (issue.author == null) {
|
||||
continue;
|
||||
}
|
||||
await insertMany(
|
||||
userModel,
|
||||
[
|
||||
{
|
||||
id: issue.author.login,
|
||||
avatarUrl: issue.author.avatarUrl,
|
||||
url: issue.author.url,
|
||||
isEmployee: assignableUsers.has(issue.author.login) ? '1' : '0',
|
||||
},
|
||||
],
|
||||
{ onConflictKey: 'id' },
|
||||
);
|
||||
|
||||
await insertMany(
|
||||
issueModel,
|
||||
[
|
||||
{
|
||||
id: issue.id,
|
||||
title: issue.title,
|
||||
body: issue.body,
|
||||
url: issue.url,
|
||||
createdAt: issue.createdAt,
|
||||
updatedAt: issue.updatedAt,
|
||||
closedAt: issue.closedAt,
|
||||
authorId: issue.author.login,
|
||||
},
|
||||
],
|
||||
{ onConflictKey: 'id' },
|
||||
);
|
||||
|
||||
for (const label of issue.labels.nodes) {
|
||||
await insertMany(
|
||||
labelModel,
|
||||
[
|
||||
{
|
||||
id: label.id,
|
||||
name: label.name,
|
||||
color: label.color,
|
||||
description: label.description,
|
||||
},
|
||||
],
|
||||
{ onConflictKey: 'id' },
|
||||
);
|
||||
await insertMany(issueLabelModel, [
|
||||
{
|
||||
pullRequestId: issue.id,
|
||||
labelId: label.id,
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return new Response('Data synced', {
|
||||
status: 200,
|
||||
});
|
||||
}
|
||||
@ -1,58 +0,0 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
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';
|
||||
import { findAll } from '@/database/database';
|
||||
import { pullRequestModel, userModel } from '@/database/model';
|
||||
|
||||
interface Contributor {
|
||||
id: string;
|
||||
avatarUrl: string;
|
||||
}
|
||||
|
||||
const Contributors = async () => {
|
||||
const contributors = await findAll(userModel);
|
||||
const pullRequests = await findAll(pullRequestModel);
|
||||
|
||||
const pullRequestByAuthor = pullRequests.reduce((acc, pr) => {
|
||||
acc[pr.authorId] = acc[pr.authorId] ? acc[pr.authorId] + 1 : 1;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const fitlerContributors = contributors
|
||||
.filter((contributor) => contributor.isEmployee === '0')
|
||||
.filter(
|
||||
(contributor) =>
|
||||
![
|
||||
'dependabot',
|
||||
'cyborch',
|
||||
'emilienchvt',
|
||||
'Samox',
|
||||
'nimraahmed',
|
||||
'gitstart-app',
|
||||
].includes(contributor.id),
|
||||
)
|
||||
.map((contributor) => {
|
||||
contributor.pullRequestCount = pullRequestByAuthor[contributor.id] || 0;
|
||||
|
||||
return contributor;
|
||||
})
|
||||
.sort((a, b) => b.pullRequestCount - a.pullRequestCount)
|
||||
.filter((contributor) => contributor.pullRequestCount > 0);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Background />
|
||||
<ContentContainer>
|
||||
<Header />
|
||||
<div>
|
||||
<AvatarGrid users={fitlerContributors as Contributor[]} />
|
||||
</div>
|
||||
</ContentContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Contributors;
|
||||
@ -1,27 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { createGraphiQLFetcher } from '@graphiql/toolkit';
|
||||
import { GraphiQL } from 'graphiql';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import 'graphiql/graphiql.css';
|
||||
|
||||
// Create a named function for your component
|
||||
function GraphiQLComponent() {
|
||||
const fetcher = createGraphiQLFetcher({
|
||||
url: 'https://api.twenty.com/graphql',
|
||||
});
|
||||
|
||||
return <GraphiQL fetcher={fetcher} />;
|
||||
}
|
||||
|
||||
// Dynamically import the GraphiQL component with SSR disabled
|
||||
const GraphiQLWithNoSSR = dynamic(() => Promise.resolve(GraphiQLComponent), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const GraphQLDocs = () => {
|
||||
return <GraphiQLWithNoSSR />;
|
||||
};
|
||||
|
||||
export default GraphQLDocs;
|
||||
@ -1,63 +0,0 @@
|
||||
import { ContentContainer } from '@/app/components/ContentContainer';
|
||||
|
||||
const DeveloperDocsLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<ContentContainer>
|
||||
<div style={{ display: 'flex', flexDirection: 'row' }}>
|
||||
<div
|
||||
style={{
|
||||
borderRight: '1px solid rgba(20, 20, 20, 0.08)',
|
||||
paddingRight: '24px',
|
||||
minWidth: '200px',
|
||||
paddingTop: '48px',
|
||||
}}
|
||||
>
|
||||
<h4 style={{ textTransform: 'uppercase', color: '#B3B3B3' }}>
|
||||
Install & Maintain
|
||||
</h4>
|
||||
<a style={{ textDecoration: 'none', color: '#333' }} href="/">
|
||||
Local setup
|
||||
</a>{' '}
|
||||
<br />
|
||||
<a style={{ textDecoration: 'none', color: '#333' }} href="/">
|
||||
Self-hosting
|
||||
</a>{' '}
|
||||
<br />
|
||||
<a style={{ textDecoration: 'none', color: '#333' }} href="/">
|
||||
Upgrade guide
|
||||
</a>{' '}
|
||||
<br /> <br />
|
||||
<h4 style={{ textTransform: 'uppercase', color: '#B3B3B3' }}>
|
||||
Resources
|
||||
</h4>
|
||||
<a style={{ textDecoration: 'none', color: '#333' }} href="/">
|
||||
Contributors Guide
|
||||
</a>{' '}
|
||||
<br />
|
||||
<a
|
||||
style={{ textDecoration: 'none', color: '#333' }}
|
||||
href="/developers/docs/graphql"
|
||||
>
|
||||
GraphQL API
|
||||
</a>{' '}
|
||||
<br />
|
||||
<a
|
||||
style={{ textDecoration: 'none', color: '#333', display: 'flex' }}
|
||||
href="/developers/rest"
|
||||
>
|
||||
Rest API
|
||||
</a>
|
||||
<a style={{ textDecoration: 'none', color: '#333' }} href="/">
|
||||
Twenty UI
|
||||
</a>{' '}
|
||||
<br />
|
||||
</div>
|
||||
<div style={{ padding: '24px', minHeight: '80vh', width: '100%' }}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</ContentContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeveloperDocsLayout;
|
||||
@ -1,9 +0,0 @@
|
||||
const DeveloperDocs = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Developer Docs</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeveloperDocs;
|
||||
@ -1,33 +0,0 @@
|
||||
'use client';
|
||||
|
||||
/*import { API } from '@stoplight/elements';/
|
||||
|
||||
import '@stoplight/elements/styles.min.css';
|
||||
|
||||
/*
|
||||
const RestApiComponent = ({ openApiJson }: { openApiJson: any }) => {
|
||||
// We load spotlightTheme style using useEffect as it breaks remaining docs style
|
||||
useEffect(() => {
|
||||
const styleElement = document.createElement('style');
|
||||
// styleElement.innerHTML = spotlightTheme.toString();
|
||||
document.head.append(styleElement);
|
||||
|
||||
return () => styleElement.remove();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<API apiDescriptionDocument={JSON.stringify(openApiJson)} router="hash" />
|
||||
);
|
||||
};*/
|
||||
|
||||
const RestApi = () => {
|
||||
/* const [openApiJson, setOpenApiJson] = useState({});
|
||||
|
||||
const children = <RestApiComponent openApiJson={openApiJson} />;*/
|
||||
|
||||
return <>API</>;
|
||||
|
||||
// return <Playground setOpenApiJson={setOpenApiJson}>{children}</Playground>;
|
||||
};
|
||||
|
||||
export default RestApi;
|
||||
@ -1,9 +0,0 @@
|
||||
const Developers = () => {
|
||||
return (
|
||||
<div>
|
||||
<p>This page should probably be built on Framer</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Developers;
|
||||
Reference in New Issue
Block a user