From a36b736ea8a844964f7623460aa4ff246f1e3ac7 Mon Sep 17 00:00:00 2001 From: Yakko Majuri <38760734+yakkomajuri@users.noreply.github.com> Date: Tue, 30 Mar 2021 13:27:10 +0000 Subject: [PATCH] [EPIC] New Contributors Page (#1137) * wip contributor card * dynamic cards * fetch contributors done * finish search * better search * wip stats * hook chart to api * sort contributors before chart * better loading states * updates * handle tab change query * wip better tooltips * much better info * improvements * better disclaimer * backfilling * lower the bar for level progress --- contents/docs/recognizing-contributions.md | 54 +++++++ package.json | 2 + scripts/backfill.py | 40 +++++ scripts/contributors.py | 2 + src/components/ContributorCard/emojiKey.ts | 130 +++++++++++++++ src/components/ContributorCard/index.tsx | 153 ++++++++++++++++++ src/components/ContributorCard/style.scss | 12 ++ src/components/ContributorSearch/index.tsx | 16 ++ src/components/ContributorSearch/style.scss | 19 +++ .../contributorStatsLogic.ts | 42 +++++ src/components/ContributorsChart/index.tsx | 125 ++++++++++++++ src/components/Layout/Layout.scss | 6 + src/components/Layout/index.tsx | 8 +- src/logic/contributorsLogic.ts | 88 ++++++++++ src/mdxGlobalComponents.js | 98 +++++------ src/pages-content/community-constants.ts | 33 ++++ src/pages/contributors.tsx | 80 +++++++++ src/pages/styles/contributors.scss | 38 +++++ src/sidebars/sidebars.json | 1 + src/types.ts | 10 ++ yarn.lock | 34 +++- 21 files changed, 936 insertions(+), 55 deletions(-) create mode 100644 contents/docs/recognizing-contributions.md create mode 100644 scripts/backfill.py create mode 100644 src/components/ContributorCard/emojiKey.ts create mode 100644 src/components/ContributorCard/index.tsx create mode 100644 src/components/ContributorCard/style.scss create mode 100644 src/components/ContributorSearch/index.tsx create mode 100644 src/components/ContributorSearch/style.scss create mode 100644 src/components/ContributorsChart/contributorStatsLogic.ts create mode 100644 src/components/ContributorsChart/index.tsx create mode 100644 src/logic/contributorsLogic.ts create mode 100644 src/pages-content/community-constants.ts create mode 100644 src/pages/contributors.tsx create mode 100644 src/pages/styles/contributors.scss diff --git a/contents/docs/recognizing-contributions.md b/contents/docs/recognizing-contributions.md new file mode 100644 index 000000000..dd0f59d34 --- /dev/null +++ b/contents/docs/recognizing-contributions.md @@ -0,0 +1,54 @@ +--- +title: Recognizing Contributions +sidebar: Docs +showTitle: true +--- + +At PostHog we aim to recognize all contributions made to our open source codebases. + +We do this largely via automated processes that ensure our contributors are recognized. + +## Pull Requests + +If you submit a Pull Request to a repository under the `PostHog` organization, a bot will automatically try to send you an email with a merch code. If this fails, the bot will try to let you know in a comment so that you can email Yakko (the bot creator and babysitter) and get your merch code. + +If, after a few days of having had your PR merged, you still didn't get a merch code, you can email us at _hey@posthog.com_ and we'll sort it out! + +## Plugins + +If you build a plugin for PostHog that is accepted into our [official repository](https://github.com/PostHog/plugin-repository), we will list you as a contributor in the categories `code` and `plugin`, as well as send you some merch. + +## Non-PR contributions + +We follow the [All Contributors spec](https://allcontributors.org/docs/en/emoji-key) for recognizing contributions. This means that if you are actively engaged in discussions, open bug reports, or contribute in other ways, a PostHog team member is able to add you to our contributors list for any of the contribution types listed in the link above. + +At the moment we only provide merch for `code` (PR merged) and `plugin` (plugin accepted into official repo) contributions, however, as a contributor in a category other than those two, we'll still list you on our [README](https://github.com/PostHog/posthog#contributors-) and create a contributor card for you on our [Contributors page](/contributors). + +## Contributor Cards + +All accepted contributors get a digital contributor card from us, which you can find in our [Contributors page](/contributors). + +Here's an explanation of the contents of that card: + +### Community MVPs + +Every time we do a release, starting with version 1.22.0, we have nominated a 'Community MVP'. This is a contributor that we have chosen to give special recognition to for one or many awesome contributions to PostHog over a given release cycle. + +If you ever win one of these awards from us, they will appear as trophies on your contributor card. The number of trophies is equal to the number of times you've been named community MVP. + +### Level + +Your contributor level is determined by how many pull requests you have had merged. + +
+ +While we have created cards for all past contributors, the we have only started tracking levels from 26/03/2021, which is why your card might say 'lvl 0' even if you had a PR merged before. + +
+ +### Powers + +'Powers' refer to the types of contributions you've made to PostHog. The types of contributions available can be seen on the [All Contributors spec](https://allcontributors.org/docs/en/emoji-key). + +Contributions of type `code` are automatically provided to you for merged pull requests. All other contribution types must be manually requested by a member of the PostHog team. + diff --git a/package.json b/package.json index 3589b5310..fb64cab82 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@mdx-js/mdx": "^1.6.19", "@mdx-js/react": "^1.6.22", "antd": "^3.23.2", + "chart.js": "^2.9.4", "cross-env": "^7.0.3", "docsearch.js": "^2.6.3", "eslint-plugin-flowtype": "^2.50.3", @@ -74,6 +75,7 @@ "typescript": "^4.0.2" }, "devDependencies": { + "@types/chart.js": "^2.9.31", "@types/mdx-js__react": "^1.5.3", "@types/react-helmet": "^6.1.0", "@types/react-modal": "^3.10.6", diff --git a/scripts/backfill.py b/scripts/backfill.py new file mode 100644 index 000000000..480683638 --- /dev/null +++ b/scripts/backfill.py @@ -0,0 +1,40 @@ +import requests +import sys +import os + +# Generates copy-pasteable HTML with contributor avatars +# Used in our Team page and the PostHog/posthog README + +image_size = sys.argv[1] if len(sys.argv) > 1 else 50 +contributors_processed = set() +contributor_faces_html = '' +github_token = os.environ.get('GITHUB_PERSONAL_TOKEN', '') # Needed for rate limiting +auth_header = { 'Authorization': f'token {github_token}' } + +repos = requests.get('https://api.github.com/orgs/PostHog/repos?type=all', headers=auth_header).json() + +contributions_per_contributor = {} + +i = 0 +l = len(repos) +for repo in repos: + if repo['fork']: + continue + print(f'Processing repo {i} of {l}') + contributors = requests.get(repo['contributors_url'], headers=auth_header).json() + for contributor in contributors: + username = contributor['login'] + if username in contributions_per_contributor: + contributions_per_contributor[username] += contributor['contributions'] + else: + contributions_per_contributor[username] = contributor['contributions'] + i += 1 + +output = '' +for username, level in contributions_per_contributor.items(): + output += f"('{username}',{level}),\n" + +print(output) + + + diff --git a/scripts/contributors.py b/scripts/contributors.py index 92c675137..bef16a452 100644 --- a/scripts/contributors.py +++ b/scripts/contributors.py @@ -14,6 +14,8 @@ auth_header = { 'Authorization': f'token {github_token}' } repos = requests.get('https://api.github.com/orgs/PostHog/repos?type=all', headers=auth_header).json() for repo in repos: + if repo['fork']: + continue contributors = requests.get(repo['contributors_url'], headers=auth_header).json() for contributor in contributors: username = contributor['login'] diff --git a/src/components/ContributorCard/emojiKey.ts b/src/components/ContributorCard/emojiKey.ts new file mode 100644 index 000000000..612667457 --- /dev/null +++ b/src/components/ContributorCard/emojiKey.ts @@ -0,0 +1,130 @@ +export const emojiKey: Record = { + a11y: { + symbol: '️️️️♿️', + description: 'Accessibility', + }, + audio: { + symbol: '🔊', + description: 'Audio', + }, + blog: { + symbol: '📝', + description: 'Blogposts', + }, + bug: { + symbol: '🐛', + description: 'Bug reports' + }, + business: { + symbol: '💼', + description: 'Business development', + }, + code: { + symbol: '💻', + description: 'Code', + }, + content: { + symbol: '🖋', + description: 'Content', + }, + data: { + symbol: '🔣', + description: 'Data', + }, + design: { + symbol: '🎨', + description: 'Design', + }, + doc: { + symbol: '📖', + description: 'Documentation', + }, + eventOrganizing: { + symbol: '📋', + description: 'Event Organizing', + }, + example: { + symbol: '💡', + description: 'Examples', + }, + financial: { + symbol: '💵', + description: 'Financial', + }, + fundingFinding: { + symbol: '🔍', + description: 'Funding Finding', + }, + ideas: { + symbol: '🤔', + description: 'Ideas, Planning, & Feedback', + }, + infra: { + symbol: '🚇', + description: 'Infrastructure (Hosting, Build-Tools, etc)', + }, + maintenance: { + symbol: '🚧', + description: 'Maintenance', + }, + mentoring: { + symbol: '🧑‍🏫', + description: 'Mentoring', + }, + platform: { + symbol: '📦', + description: 'Packaging/porting to new platform', + }, + plugin: { + symbol: '🔌', + description: 'Plugins', + }, + projectManagement: { + symbol: '📆', + description: 'Project Management', + }, + question: { + symbol: '💬', + description: 'Answering Questions', + }, + research: { + symbol: '🔬', + description: 'Research', + }, + review: { + symbol: '👀', + description: 'Reviewed Pull Requests', + }, + security: { + symbol: '🛡️', + description: 'Security', + }, + talk: { + symbol: '📢', + description: 'Talks', + }, + test: { + symbol: '⚠️', + description: 'Tests', + }, + tool: { + symbol: '🔧', + description: 'Tools', + }, + translation: { + symbol: '🌍', + description: 'Translation', + }, + tutorial: { + symbol: '✅', + description: 'Tutorials', + }, + userTesting: { + symbol: '📓', + description: 'User Testing', + }, + video: { + symbol: '📹', + description: 'Videos', + }, + } \ No newline at end of file diff --git a/src/components/ContributorCard/index.tsx b/src/components/ContributorCard/index.tsx new file mode 100644 index 000000000..194ee06ee --- /dev/null +++ b/src/components/ContributorCard/index.tsx @@ -0,0 +1,153 @@ +import React from 'react' +import { Card, Col, Progress, Tag, Tooltip } from 'antd' +import { Link } from 'gatsby' +import './style.scss' +import { emojiKey } from './emojiKey' +import { Spacer } from 'components/Spacer' + +interface ContributorCardStructureMeta { + name: string + imageSrc: string + contributions: string[] + mvpWins: number + contributorLevel: number +} + +interface ContributorCardMeta extends ContributorCardStructureMeta { + link: string + onClick?: () => void | undefined +} + +const ContributorCardStructure = ({ + name, + imageSrc, + contributions, + mvpWins, + contributorLevel, +}: ContributorCardStructureMeta) => { + const handleTooltipContentClick = (e: React.MouseEvent, pageKey: string = '') => { + if (window) { + e.preventDefault() + e.stopPropagation() + window.open( + `${window.location.protocol}//${window.location.host}/docs/recognizing-contributions#${pageKey}`, + '_blank' + ) + } + } + + const ContributorCardTooltip = ({ + title, + children, + pageKey, + }: { + title: string + children: React.ReactNode + pageKey: string + }) => ( + + handleTooltipContentClick(e, pageKey)} className="tooltip-content"> + {children} + + + ) + + return ( + + + {mvpWins > 0 ? ( + + +

+ {Array.from({ length: mvpWins }).map((_: any, i: number) => ( + 🏆 + ))} +

+
+
+ ) : null} + + contributor image + +
+ +
+ {name} +
+ +

lvl {contributorLevel}

+ = 50 ? 50 : (100 * contributorLevel) / 50} + className="progress-bar" + showInfo={false} + /> +
+ + +

Powers

+
+ +

+ {contributions.map((key) => ( + + + {emojiKey[key].symbol} + {' '} + + ))} +

+
+ + ) +} + +export const ContributorCard = ({ + name, + link, + imageSrc, + onClick, + contributions, + mvpWins, + contributorLevel, +}: ContributorCardMeta) => { + const ContributorDetails = () => ( + + ) + + return ( +
+ {onClick ? ( + + + + ) : link.includes('.') ? ( + + + + ) : ( + + + + )} +
+ ) +} diff --git a/src/components/ContributorCard/style.scss b/src/components/ContributorCard/style.scss new file mode 100644 index 000000000..0c4166ffb --- /dev/null +++ b/src/components/ContributorCard/style.scss @@ -0,0 +1,12 @@ +.contributor-card-wrapper { + .ant-card-bordered { + border: 1px solid #653c9a !important; + border-radius: 20px; + } + + .tooltip-content:hover, + .tooltip-content:hover span { + background-color: #271046 !important; + cursor: pointer; + } +} diff --git a/src/components/ContributorSearch/index.tsx b/src/components/ContributorSearch/index.tsx new file mode 100644 index 000000000..f70268291 --- /dev/null +++ b/src/components/ContributorSearch/index.tsx @@ -0,0 +1,16 @@ +import React from 'react' +import { Input } from 'antd' +import './style.scss' +import { useActions } from 'kea' +import { contributorsLogic } from 'logic/contributorsLogic' + +export const ContributorSearch = () => { + const { processSearchInput } = useActions(contributorsLogic) + return ( + processSearchInput(e.target.value)} + /> + ) +} diff --git a/src/components/ContributorSearch/style.scss b/src/components/ContributorSearch/style.scss new file mode 100644 index 000000000..e0fe664c6 --- /dev/null +++ b/src/components/ContributorSearch/style.scss @@ -0,0 +1,19 @@ +.contributor-search { + max-width: 300px; + margin-bottom: 20px; + + input { + background-color: rgb(45, 21, 80); + border: 1px solid #683f9e; + color: #dfdfdf; + } + + input:hover, + input:focus { + border-color: #9769d3 !important; + } + + i { + color: #cd8ff2; + } +} diff --git a/src/components/ContributorsChart/contributorStatsLogic.ts b/src/components/ContributorsChart/contributorStatsLogic.ts new file mode 100644 index 000000000..cee6ca4c7 --- /dev/null +++ b/src/components/ContributorsChart/contributorStatsLogic.ts @@ -0,0 +1,42 @@ +import { kea } from 'kea' + +interface Dataset { + labels: string[] + breakdown_value: string + data: number[] +} + +export const contributorStatsLogic = kea({ + loaders: { + datasets: [ + [] as Dataset[], + { + loadDatasets: async () => { + const datasetsRes = await fetch( + 'https://app.posthog.com/api/dashboard/2868/?share_token=6j6-3tr86CgbNK_4PmyYHxHQYTdvEg' + ) + const datasetsJson = await datasetsRes.json() + + const sortedDatasets = datasetsJson.items[0].result.sort((a: Dataset, b: Dataset) => { + const aTotal = a.data.reduce((aggregate, current) => aggregate + current) + const bTotal = b.data.reduce((aggregate, current) => aggregate + current) + if (bTotal > aTotal) { + return 1 + } + return -1 + }) + + return sortedDatasets.slice(0, 15) + }, + }, + ], + }, + events: ({ actions }) => ({ + afterMount: () => { + // only load in the frontend + if (typeof window !== 'undefined') { + actions.loadDatasets() + } + }, + }), +}) diff --git a/src/components/ContributorsChart/index.tsx b/src/components/ContributorsChart/index.tsx new file mode 100644 index 000000000..6abc81301 --- /dev/null +++ b/src/components/ContributorsChart/index.tsx @@ -0,0 +1,125 @@ +import React, { useRef, useEffect } from 'react' +import Chart from 'chart.js' +import { useValues } from 'kea' +import { contributorStatsLogic } from './contributorStatsLogic' +import { Spacer } from 'components/Spacer' +import { Link } from 'gatsby' + +export const ContributorsChart = () => { + const canvasRef = useRef(null) + const { datasets, datasetsLoading } = useValues(contributorStatsLogic) + + const lineColorsList = [ + '#CCA6FF', + '#BD8AFF', + '#AC6CFF', + '#9A4EFF', + '#892FFF', + '#7811FF', + '#6900F2', + '#5C00D4', + '#4F00B5', + '#420097', + '#340079', + '#DCC1FF', + '#E3CDFF', + '#D1AEFF', + '#D8BBFF', + ] + + useEffect(() => { + if (canvasRef.current) { + const canvas = canvasRef.current + const context = canvas.getContext('2d') + if (context && datasets.length > 0) { + const datasetList = [] + let i = 0 + for (const set of datasets) { + if (i === lineColorsList.length) { + break + } + datasetList.push({ + label: set.breakdown_value, + data: set.data, + fill: false, + showLine: true, + backgroundColor: lineColorsList[i], + borderColor: lineColorsList[i], + borderWidth: 1, + }) + ++i + } + + const labels = datasets[0].labels.map((label: string) => { + const splitLabel = label.split(' ') + return splitLabel[splitLabel.length - 1] + }) + + new Chart(context, { + type: 'line', + + data: { + labels: labels, + datasets: datasetList, + }, + options: { + responsive: true, + title: { + display: false, + text: 'Top 15 PostHog Contributors', + fontColor: '#dedede', + fontSize: 18, + }, + tooltips: { + mode: 'index', + intersect: false, + }, + hover: { + mode: 'nearest', + intersect: true, + }, + scales: { + xAxes: [ + { + display: true, + scaleLabel: { + display: true, + labelString: 'Month', + }, + }, + ], + yAxes: [ + { + display: true, + scaleLabel: { + display: true, + labelString: 'Value', + }, + }, + ], + }, + }, + }) + } + } + }, [datasets]) + + return ( + <> + {datasetsLoading ? ( + + ) : ( + <> +
Top 15 PostHog Contributors
+ + + ⚠️ Only displaying contributions from after 29/03/2021 + + + + + + )} + + ) +} diff --git a/src/components/Layout/Layout.scss b/src/components/Layout/Layout.scss index 6349b0d7e..8bbf29eca 100644 --- a/src/components/Layout/Layout.scss +++ b/src/components/Layout/Layout.scss @@ -1594,3 +1594,9 @@ div::-webkit-scrollbar-thumb:hover { .text-center { text-align: center; } + +.contributor-card-tooltip { + .ant-tooltip-inner { + background-color: #0f041f; + } +} diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx index 5dda4562a..3d576b450 100644 --- a/src/components/Layout/index.tsx +++ b/src/components/Layout/index.tsx @@ -35,12 +35,10 @@ const Layout = ({ onPostPage = false, pageTitle = '', isDocsPage = false, - isHomePage = false, isBlogArticlePage = false, children, className = '', containerStyle = {}, - menuActiveKey = '', }: LayoutProps) => { const { sidebarHide, anchorHide } = useValues(layoutLogic) const { posthog } = useValues(posthogAnalyticsLogic) @@ -66,10 +64,6 @@ const Layout = ({ <>
{onPostPage && !sidebarHide && !isBlogArticlePage && ( @@ -150,7 +144,7 @@ const Layout = ({ {isBlogArticlePage && } -