Marketing improvements 3 (#3175)

* Improve marketing website

* User guide with icons

* Add TOC

* Linter

* Basic GraphQL playground

* Very basic contributors page

* Failed attempt to integrate REST playground

* Yarn

* Begin contributors DB

* Improve contributors page
This commit is contained in:
Félix Malfait
2023-12-29 11:17:32 +01:00
committed by GitHub
parent fa8a04743c
commit c422045ea6
46 changed files with 7589 additions and 687 deletions

View File

@ -0,0 +1,19 @@
import Database from 'better-sqlite3';
export async function GET(
request: Request,
{ params }: { params: { slug: string } }) {
const db = new Database('db.sqlite', { readonly: true });
if(params.slug !== 'users' && params.slug !== 'labels' && params.slug !== 'pullRequests') {
return Response.json({ error: 'Invalid table name' }, { status: 400 });
}
const rows = db.prepare('SELECT * FROM ' + params.slug).all();
db.close();
return Response.json(rows);
}

View File

@ -0,0 +1,288 @@
import Database from 'better-sqlite3';
import { graphql } from '@octokit/graphql';
const db = new Database('db.sqlite', { verbose: console.log });
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;
createdAt: string;
updatedAt: string;
closedAt: string;
mergedAt: string;
author: AuthorNode;
labels: {
nodes: LabelNode[];
};
}
interface IssueNode {
id: string;
title: string;
body: 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 ' + 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
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
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));
}
const initDb = () => {
db.prepare(`
CREATE TABLE IF NOT EXISTS pullRequests (
id TEXT PRIMARY KEY,
title TEXT,
body TEXT,
createdAt TEXT,
updatedAt TEXT,
closedAt TEXT,
mergedAt TEXT,
authorId TEXT,
FOREIGN KEY (authorId) REFERENCES users(id)
);
`).run();
db.prepare(`
CREATE TABLE IF NOT EXISTS issues (
id TEXT PRIMARY KEY,
title TEXT,
body TEXT,
createdAt TEXT,
updatedAt TEXT,
closedAt TEXT,
authorId TEXT,
FOREIGN KEY (authorId) REFERENCES users(id)
);
`).run();
db.prepare(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
login TEXT,
avatarUrl TEXT,
url TEXT,
isEmployee BOOLEAN
);
`).run();
db.prepare(`
CREATE TABLE IF NOT EXISTS labels (
id TEXT PRIMARY KEY,
name TEXT,
color TEXT,
description TEXT
);
`).run();
db.prepare(`
CREATE TABLE IF NOT EXISTS pullRequestLabels (
pullRequestId TEXT,
labelId TEXT,
FOREIGN KEY (pullRequestId) REFERENCES pullRequests(id),
FOREIGN KEY (labelId) REFERENCES labels(id)
);
`).run();
db.prepare(`
CREATE TABLE IF NOT EXISTS issueLabels (
issueId TEXT,
labelId TEXT,
FOREIGN KEY (issueId) REFERENCES issues(id),
FOREIGN KEY (labelId) REFERENCES labels(id)
);
`).run();
};
export async function GET() {
initDb();
// TODO if we ever hit API Rate Limiting
const lastPRCursor = null;
const lastIssueCursor = null;
const assignableUsers = await fetchAssignableUsers();
const prs = await fetchData(lastPRCursor) as Array<PullRequestNode>;
const issues = await fetchData(lastIssueCursor) as Array<IssueNode>;
const insertPR = db.prepare('INSERT INTO pullRequests (id, title, body, createdAt, updatedAt, closedAt, mergedAt, authorId) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO NOTHING');
const insertIssue = db.prepare('INSERT INTO issues (id, title, body, createdAt, updatedAt, closedAt, authorId) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO NOTHING');
const insertUser = db.prepare('INSERT INTO users (id, login, avatarUrl, url, isEmployee) VALUES (?, ?, ?, ?, ?) ON CONFLICT(id) DO NOTHING');
const insertLabel = db.prepare('INSERT INTO labels (id, name, color, description) VALUES (?, ?, ?, ?) ON CONFLICT(id) DO NOTHING');
const insertPullRequestLabel = db.prepare('INSERT INTO pullRequestLabels (pullRequestId, labelId) VALUES (?, ?)');
const insertIssueLabel = db.prepare('INSERT INTO issueLabels (issueId, labelId) VALUES (?, ?)');
for (const pr of prs) {
console.log(pr);
if(pr.author == null) { continue; }
insertUser.run(pr.author.resourcePath, pr.author.login, pr.author.avatarUrl, pr.author.url, assignableUsers.has(pr.author.login) ? 1 : 0);
insertPR.run(pr.id, pr.title, pr.body, pr.createdAt, pr.updatedAt, pr.closedAt, pr.mergedAt, pr.author.resourcePath);
for (const label of pr.labels.nodes) {
insertLabel.run(label.id, label.name, label.color, label.description);
insertPullRequestLabel.run(pr.id, label.id);
}
}
for (const issue of issues) {
if(issue.author == null) { continue; }
insertUser.run(issue.author.resourcePath, issue.author.login, issue.author.avatarUrl, issue.author.url, assignableUsers.has(issue.author.login) ? 1 : 0);
insertIssue.run(issue.id, issue.title, issue.body, issue.createdAt, issue.updatedAt, issue.closedAt, issue.author.resourcePath);
for (const label of issue.labels.nodes) {
insertLabel.run(label.id, label.name, label.color, label.description);
insertIssueLabel.run(issue.id, label.id);
}
}
db.close();
return new Response("Data synced", { status: 200 });
};

View File

@ -0,0 +1,40 @@
import Image from 'next/image';
import Database from 'better-sqlite3';
import AvatarGrid from '@/app/components/AvatarGrid';
interface Contributor {
login: string;
avatarUrl: string;
pullRequestCount: number;
}
const Contributors = async () => {
const db = new Database('db.sqlite', { readonly: true });
const contributors = db.prepare(`SELECT
u.login,
u.avatarUrl,
COUNT(pr.id) AS pullRequestCount
FROM
users u
JOIN
pullRequests pr ON u.id = pr.authorId
GROUP BY
u.id
ORDER BY
pullRequestCount DESC;
`).all() as Contributor[];
db.close();
return (
<div>
<h1>Top Contributors</h1>
<AvatarGrid users={contributors} />
</div>
);
};
export default Contributors;

View File

@ -0,0 +1,26 @@
'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;

View File

@ -0,0 +1,63 @@
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;

View File

@ -0,0 +1,9 @@
const DeveloperDocs = () => {
return (
<div>
<h1>Developer Docs</h1>
</div>
);
};
export default DeveloperDocs;

View File

@ -0,0 +1,33 @@
'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;

View File

@ -0,0 +1,9 @@
const Developers = () => {
return (
<div>
<p>This page should probably be built on Framer</p>
</div>
);
};
export default Developers;