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:
@ -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);
|
||||
}
|
||||
@ -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 });
|
||||
};
|
||||
@ -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;
|
||||
@ -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;
|
||||
63
packages/twenty-website/src/app/developers/docs/layout.tsx
Normal file
63
packages/twenty-website/src/app/developers/docs/layout.tsx
Normal 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;
|
||||
9
packages/twenty-website/src/app/developers/docs/page.tsx
Normal file
9
packages/twenty-website/src/app/developers/docs/page.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
const DeveloperDocs = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Developer Docs</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeveloperDocs;
|
||||
@ -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;
|
||||
9
packages/twenty-website/src/app/developers/page.tsx
Normal file
9
packages/twenty-website/src/app/developers/page.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
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