mirror of
https://github.com/BillyOutlast/posthog.com.git
synced 2026-02-04 03:11:21 +01:00
[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
This commit is contained in:
54
contents/docs/recognizing-contributions.md
Normal file
54
contents/docs/recognizing-contributions.md
Normal file
@@ -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.
|
||||
|
||||
<blockquote class='warning-note'>
|
||||
|
||||
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.
|
||||
|
||||
</blockquote>
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
40
scripts/backfill.py
Normal file
40
scripts/backfill.py
Normal file
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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']
|
||||
|
||||
130
src/components/ContributorCard/emojiKey.ts
Normal file
130
src/components/ContributorCard/emojiKey.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
export const emojiKey: Record<string, any> = {
|
||||
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',
|
||||
},
|
||||
}
|
||||
153
src/components/ContributorCard/index.tsx
Normal file
153
src/components/ContributorCard/index.tsx
Normal file
@@ -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
|
||||
}) => (
|
||||
<Tooltip title={title} overlayClassName="contributor-card-tooltip">
|
||||
<span onClick={(e) => handleTooltipContentClick(e, pageKey)} className="tooltip-content">
|
||||
{children}
|
||||
</span>
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
return (
|
||||
<Col sm={12} md={12} lg={8} xl={6} style={{ marginBottom: 20 }}>
|
||||
<Card
|
||||
style={{ height: 450, display: 'flex', marginBottom: 20 }}
|
||||
bodyStyle={{ display: 'flex', flexDirection: 'column', flexGrow: 1 }}
|
||||
className="card-elevated"
|
||||
>
|
||||
{mvpWins > 0 ? (
|
||||
<Tag color="transparent" style={{ maxWidth: '30%', position: 'absolute', right: 15, top: 15 }}>
|
||||
<ContributorCardTooltip title={`Community MVP ${mvpWins}x`} pageKey="community-mvps">
|
||||
<h4>
|
||||
{Array.from({ length: mvpWins }).map((_: any, i: number) => (
|
||||
<span key={`trophy_${i}`}>🏆</span>
|
||||
))}
|
||||
</h4>
|
||||
</ContributorCardTooltip>
|
||||
</Tag>
|
||||
) : null}
|
||||
|
||||
<img
|
||||
src={imageSrc}
|
||||
style={{ maxWidth: 60, maxHeight: 60, marginBottom: 0, borderRadius: 10 }}
|
||||
className="center"
|
||||
alt="contributor image"
|
||||
/>
|
||||
|
||||
<br />
|
||||
|
||||
<h5 className="centered" style={{ color: '#fff' }}>
|
||||
{name}
|
||||
</h5>
|
||||
<ContributorCardTooltip title="Number of PRs merged" pageKey="level">
|
||||
<p style={{ color: 'rgb(231 184 250)', marginBottom: 5 }}>lvl {contributorLevel}</p>
|
||||
<Progress
|
||||
strokeColor={{
|
||||
'0%': '#220f3f',
|
||||
'100%': '#ab75ff',
|
||||
}}
|
||||
percent={contributorLevel >= 50 ? 50 : (100 * contributorLevel) / 50}
|
||||
className="progress-bar"
|
||||
showInfo={false}
|
||||
/>
|
||||
</ContributorCardTooltip>
|
||||
<Spacer height={40} />
|
||||
<ContributorCardTooltip title="Types of contributions made" pageKey="powers">
|
||||
<p style={{ color: 'rgb(231 184 250)', fontSize: 20, marginBottom: 0 }}>Powers</p>
|
||||
</ContributorCardTooltip>
|
||||
<Spacer height={20} />
|
||||
<h2>
|
||||
{contributions.map((key) => (
|
||||
<span key={key}>
|
||||
<ContributorCardTooltip title={emojiKey[key].description} pageKey="powers">
|
||||
{emojiKey[key].symbol}
|
||||
</ContributorCardTooltip>{' '}
|
||||
</span>
|
||||
))}
|
||||
</h2>
|
||||
</Card>
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
||||
export const ContributorCard = ({
|
||||
name,
|
||||
link,
|
||||
imageSrc,
|
||||
onClick,
|
||||
contributions,
|
||||
mvpWins,
|
||||
contributorLevel,
|
||||
}: ContributorCardMeta) => {
|
||||
const ContributorDetails = () => (
|
||||
<ContributorCardStructure
|
||||
name={name}
|
||||
imageSrc={imageSrc}
|
||||
contributions={contributions}
|
||||
mvpWins={mvpWins}
|
||||
contributorLevel={contributorLevel}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="contributor-card-wrapper">
|
||||
{onClick ? (
|
||||
<span onClick={onClick}>
|
||||
<ContributorDetails />
|
||||
</span>
|
||||
) : link.includes('.') ? (
|
||||
<a href={link}>
|
||||
<ContributorDetails />
|
||||
</a>
|
||||
) : (
|
||||
<Link to={link}>
|
||||
<ContributorDetails />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
12
src/components/ContributorCard/style.scss
Normal file
12
src/components/ContributorCard/style.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
16
src/components/ContributorSearch/index.tsx
Normal file
16
src/components/ContributorSearch/index.tsx
Normal file
@@ -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 (
|
||||
<Input.Search
|
||||
className="contributor-search"
|
||||
size="large"
|
||||
onChange={(e) => processSearchInput(e.target.value)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
19
src/components/ContributorSearch/style.scss
Normal file
19
src/components/ContributorSearch/style.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
42
src/components/ContributorsChart/contributorStatsLogic.ts
Normal file
42
src/components/ContributorsChart/contributorStatsLogic.ts
Normal file
@@ -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()
|
||||
}
|
||||
},
|
||||
}),
|
||||
})
|
||||
125
src/components/ContributorsChart/index.tsx
Normal file
125
src/components/ContributorsChart/index.tsx
Normal file
@@ -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<HTMLCanvasElement>(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 ? (
|
||||
<Spacer height={800} />
|
||||
) : (
|
||||
<>
|
||||
<h5 style={{ margin: 0, color: '#efefef' }}>Top 15 PostHog Contributors</h5>
|
||||
<Link to="/docs/recognizing-contributions">
|
||||
<small style={{ color: '#dedede' }}>
|
||||
⚠️ Only displaying contributions from after 29/03/2021
|
||||
</small>
|
||||
</Link>
|
||||
<Spacer height={10} />
|
||||
<canvas ref={canvasRef} style={{ maxWidth: 1000, maxHeight: 800 }} className="center centered" />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1594,3 +1594,9 @@ div::-webkit-scrollbar-thumb:hover {
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.contributor-card-tooltip {
|
||||
.ant-tooltip-inner {
|
||||
background-color: #0f041f;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = ({
|
||||
<>
|
||||
<Header
|
||||
onPostPage={onPostPage}
|
||||
isBlogArticlePage={isBlogArticlePage}
|
||||
isHomePage={isHomePage}
|
||||
isDocsPage={isDocsPage}
|
||||
menuActiveKey={menuActiveKey ? menuActiveKey : isDocsPage ? 'docs' : ''}
|
||||
/>
|
||||
<AntdLayout id="antd-main-layout-wrapper" hasSider>
|
||||
{onPostPage && !sidebarHide && !isBlogArticlePage && (
|
||||
@@ -150,7 +144,7 @@ const Layout = ({
|
||||
</AntdLayout>
|
||||
<AntdLayout style={{ background: '#ffffff' }}>
|
||||
{isBlogArticlePage && <BlogFooter />}
|
||||
<Footer isDocsPage={isDocsPage} onPostPage={onPostPage} />
|
||||
<Footer onPostPage={onPostPage} />
|
||||
</AntdLayout>
|
||||
<PosthogAnnouncement />
|
||||
<GetStartedModal />
|
||||
|
||||
88
src/logic/contributorsLogic.ts
Normal file
88
src/logic/contributorsLogic.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { kea, BreakPointFunction } from 'kea'
|
||||
import { Contributor } from 'types'
|
||||
import { ignoreContributors, mvpWinners } from '../pages-content/community-constants'
|
||||
|
||||
export const contributorsLogic = kea({
|
||||
actions: {
|
||||
processSearchInput: (query: string) => ({ query }),
|
||||
setSearchQuery: (query: string) => ({ query }),
|
||||
},
|
||||
reducers: {
|
||||
searchQuery: [
|
||||
'',
|
||||
{
|
||||
setSearchQuery: (_: null, { query }: { query: string }) => query,
|
||||
},
|
||||
],
|
||||
},
|
||||
listeners: ({ actions }) => ({
|
||||
processSearchInput: async ({ query }: { query: string }, breakpoint: BreakPointFunction) => {
|
||||
// pause for 100ms and break if `setUsername`
|
||||
// was called again during this time
|
||||
await breakpoint(100)
|
||||
|
||||
actions.setSearchQuery(query)
|
||||
},
|
||||
}),
|
||||
selectors: {
|
||||
filteredContributors: [
|
||||
(s) => [s.searchQuery, s.contributors],
|
||||
(searchQuery: string, contributors: Contributor[]) =>
|
||||
contributors.filter((contributor: Contributor) => contributor.login.includes(searchQuery)),
|
||||
],
|
||||
},
|
||||
loaders: {
|
||||
contributors: [
|
||||
[] as Contributor[],
|
||||
{
|
||||
loadContributors: async () => {
|
||||
const contributorsRes = await fetch(
|
||||
'https://raw.githubusercontent.com/PostHog/posthog/master/.all-contributorsrc#'
|
||||
)
|
||||
const fileContent = await contributorsRes.text()
|
||||
const parsedContent = JSON.parse(fileContent.replace(/"badgeTemplate": ".*",/, ''))
|
||||
|
||||
let contributors = parsedContent.contributors.filter(
|
||||
(contributor: Contributor) => !ignoreContributors.has(contributor.login)
|
||||
)
|
||||
|
||||
const contributorLevelsRes = await fetch(
|
||||
'https://data.heroku.com/dataclips/zzzdzthiltszdhfliisfdhpqxocr.json'
|
||||
)
|
||||
const contributorLevelsJson = await contributorLevelsRes.json()
|
||||
|
||||
const levelMap: Record<string, number> = {}
|
||||
|
||||
for (const row of contributorLevelsJson.values) {
|
||||
levelMap[row[0]] = row[1]
|
||||
}
|
||||
|
||||
for (let contributor of contributors) {
|
||||
contributor['level'] = levelMap[contributor.login] || 0
|
||||
contributor['mvpWins'] = mvpWinners[contributor.login] || 0
|
||||
}
|
||||
|
||||
const sortedContributors = contributors.sort((a: Contributor, b: Contributor) => {
|
||||
if (b.level > a.level) {
|
||||
return 1
|
||||
} else if (a.level === b.level) {
|
||||
return b.mvpWins > a.mvpWins ? 1 : -1
|
||||
} else {
|
||||
return -1
|
||||
}
|
||||
})
|
||||
|
||||
return sortedContributors
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
events: ({ actions }) => ({
|
||||
afterMount: () => {
|
||||
// only load in the frontend
|
||||
if (typeof window !== 'undefined') {
|
||||
actions.loadContributors()
|
||||
}
|
||||
},
|
||||
}),
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
// AUTO GENERATED FILE
|
||||
// AUTO GENERATED FILE
|
||||
|
||||
import { ArrayCTA } from './components/ArrayCTA'
|
||||
import { BasicHedgehogImage } from './components/BasicHedgehogImage'
|
||||
@@ -8,6 +8,9 @@ import { CodeBlock } from './components/CodeBlock'
|
||||
import { CompensationCalculator } from './components/CompensationCalculator'
|
||||
import { Container } from './components/Container'
|
||||
import { ContributorAvatars } from './components/ContributorAvatars'
|
||||
import { ContributorCard } from './components/ContributorCard'
|
||||
import { ContributorSearch } from './components/ContributorSearch'
|
||||
import { ContributorsChart } from './components/ContributorsChart'
|
||||
import { CornerBrackets } from './components/CornerBrackets'
|
||||
import { DarkModeToggle } from './components/DarkModeToggle'
|
||||
import { DemoScheduler } from './components/DemoScheduler'
|
||||
@@ -46,48 +49,51 @@ import { StartNowButton } from './components/StartNowButton'
|
||||
import { TableOfContents } from './components/TableOfContents'
|
||||
|
||||
export const shortcodes = {
|
||||
ArrayCTA,
|
||||
BasicHedgehogImage,
|
||||
BlogFooter,
|
||||
CallToAction,
|
||||
CodeBlock,
|
||||
CompensationCalculator,
|
||||
Container,
|
||||
ContributorAvatars,
|
||||
CornerBrackets,
|
||||
DarkModeToggle,
|
||||
DemoScheduler,
|
||||
DocsPageSurvey,
|
||||
DocsSearch,
|
||||
FeaturesComparisonTable,
|
||||
FeaturesNav,
|
||||
Footer,
|
||||
GetStartedModal,
|
||||
HiddenSection,
|
||||
Features,
|
||||
Hero,
|
||||
LandingPageCallToAction,
|
||||
PrivateCloud,
|
||||
ProductFeatureIcons,
|
||||
RecentBlogPosts,
|
||||
Roadmap,
|
||||
SocialProof,
|
||||
Tutorials,
|
||||
NewsletterForm,
|
||||
OtherFeaturesBlock,
|
||||
PageHeader,
|
||||
PostCard,
|
||||
PricingComparisonTable,
|
||||
PricingSlider,
|
||||
ResponsiveAnchor,
|
||||
ResponsiveSidebar,
|
||||
ResponsiveTopBar,
|
||||
DesignedForYourStackBlock,
|
||||
FeaturedSectionTextLeft,
|
||||
FeaturedSectionTextRight,
|
||||
FeaturedSectionTripleImage,
|
||||
Spacer,
|
||||
StarRepoButton,
|
||||
StartNowButton,
|
||||
TableOfContents,
|
||||
}
|
||||
ArrayCTA,
|
||||
BasicHedgehogImage,
|
||||
BlogFooter,
|
||||
CallToAction,
|
||||
CodeBlock,
|
||||
CompensationCalculator,
|
||||
Container,
|
||||
ContributorAvatars,
|
||||
ContributorCard,
|
||||
ContributorSearch,
|
||||
ContributorsChart,
|
||||
CornerBrackets,
|
||||
DarkModeToggle,
|
||||
DemoScheduler,
|
||||
DocsPageSurvey,
|
||||
DocsSearch,
|
||||
FeaturesComparisonTable,
|
||||
FeaturesNav,
|
||||
Footer,
|
||||
GetStartedModal,
|
||||
HiddenSection,
|
||||
Features,
|
||||
Hero,
|
||||
LandingPageCallToAction,
|
||||
PrivateCloud,
|
||||
ProductFeatureIcons,
|
||||
RecentBlogPosts,
|
||||
Roadmap,
|
||||
SocialProof,
|
||||
Tutorials,
|
||||
NewsletterForm,
|
||||
OtherFeaturesBlock,
|
||||
PageHeader,
|
||||
PostCard,
|
||||
PricingComparisonTable,
|
||||
PricingSlider,
|
||||
ResponsiveAnchor,
|
||||
ResponsiveSidebar,
|
||||
ResponsiveTopBar,
|
||||
DesignedForYourStackBlock,
|
||||
FeaturedSectionTextLeft,
|
||||
FeaturedSectionTextRight,
|
||||
FeaturedSectionTripleImage,
|
||||
Spacer,
|
||||
StarRepoButton,
|
||||
StartNowButton,
|
||||
TableOfContents
|
||||
}
|
||||
33
src/pages-content/community-constants.ts
Normal file
33
src/pages-content/community-constants.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
// org members are private, can't pull this in the frontend without a gh token
|
||||
|
||||
export const ignoreContributors = new Set([
|
||||
'timgl',
|
||||
'mariusandra',
|
||||
'yakkomajuri',
|
||||
'macobo',
|
||||
'jamesefhawkins',
|
||||
'posthog-bot',
|
||||
'dependabot',
|
||||
'dependabot-preview[bot]',
|
||||
'paolodamico',
|
||||
'Twixes',
|
||||
'EDsCODE',
|
||||
'fuziontech',
|
||||
'dependabot[bot]',
|
||||
'posthog-contributions-bot[bot]',
|
||||
'piemets',
|
||||
'lottiecoxon',
|
||||
'berntgl',
|
||||
'seanpackham',
|
||||
'corywatilo',
|
||||
'eltjehelene',
|
||||
'kpthatsme',
|
||||
'ungless',
|
||||
'Tannergoods',
|
||||
'mikenicklas',
|
||||
])
|
||||
|
||||
export const mvpWinners: Record<string, number> = {
|
||||
samcaspus: 1,
|
||||
oshura3: 1,
|
||||
}
|
||||
80
src/pages/contributors.tsx
Normal file
80
src/pages/contributors.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import React, { useState } from 'react'
|
||||
import Layout from '../components/Layout'
|
||||
import { Spacer } from '../components/Spacer'
|
||||
import { Row, Tabs, Spin } from 'antd'
|
||||
import { useActions, useValues } from 'kea'
|
||||
import { contributorsLogic } from '../logic/contributorsLogic'
|
||||
import { SEO } from '../components/seo'
|
||||
import pluginLibraryOgImage from '../images/posthog-plugins.png'
|
||||
import './styles/contributors.scss'
|
||||
import { ContributorCard } from 'components/ContributorCard'
|
||||
import { Contributor } from 'types'
|
||||
import { ContributorSearch } from 'components/ContributorSearch'
|
||||
import { ContributorsChart } from 'components/ContributorsChart'
|
||||
|
||||
const { TabPane } = Tabs
|
||||
|
||||
export const ContributorsPage = () => {
|
||||
const { setSearchQuery } = useActions(contributorsLogic)
|
||||
const { filteredContributors, contributorsLoading } = useValues(contributorsLogic)
|
||||
const [activeTab, setActiveTab] = useState('list')
|
||||
|
||||
const handleTabClick = (newTab: string) => {
|
||||
setActiveTab(newTab)
|
||||
if (newTab === 'list') {
|
||||
setSearchQuery('')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="contributors-page-wrapper">
|
||||
<Layout>
|
||||
<SEO
|
||||
title="Contributors • PostHog"
|
||||
description="Plugins for getting data in and out of PostHog, the open source product analytics platform."
|
||||
image={pluginLibraryOgImage}
|
||||
/>
|
||||
<div className="centered" style={{ margin: 'auto' }}>
|
||||
<Spacer />
|
||||
<h1 className="center">Contributors</h1>
|
||||
<Tabs activeKey={activeTab} onChange={(key) => handleTabClick(key)}>
|
||||
<TabPane tab="List" key="list" />
|
||||
<TabPane tab="Stats" key="stats" />
|
||||
</Tabs>
|
||||
<Spacer height={20} />
|
||||
|
||||
{activeTab === 'list' ? (
|
||||
<>
|
||||
<ContributorSearch />
|
||||
<Spacer height={20} />
|
||||
<Row gutter={16} style={{ marginTop: 16, marginRight: 10, marginLeft: 10, minHeight: 600 }}>
|
||||
{contributorsLoading ? (
|
||||
<Spin size="large" style={{ position: 'fixed', top: '50%', left: '50%' }} />
|
||||
) : (
|
||||
<>
|
||||
{filteredContributors.map((contributor: Contributor) => (
|
||||
<ContributorCard
|
||||
key={contributor.login}
|
||||
name={contributor.login}
|
||||
link={contributor.profile}
|
||||
imageSrc={contributor.avatar_url}
|
||||
contributions={contributor.contributions}
|
||||
mvpWins={contributor.mvpWins}
|
||||
contributorLevel={contributor.level}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</Row>
|
||||
</>
|
||||
) : (
|
||||
<ContributorsChart />
|
||||
)}
|
||||
</div>
|
||||
<Spacer />
|
||||
</Layout>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ContributorsPage
|
||||
38
src/pages/styles/contributors.scss
Normal file
38
src/pages/styles/contributors.scss
Normal file
@@ -0,0 +1,38 @@
|
||||
.contributors-page-wrapper {
|
||||
.ant-layout {
|
||||
background-color: #220f3f !important;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
h1 {
|
||||
line-height: 15px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.card-elevated {
|
||||
background-color: #19082f;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
.ant-progress-inner {
|
||||
background: #220f3f;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-nav .ant-tabs-tab-active,
|
||||
.ant-tabs-tab:hover {
|
||||
color: #cca6ff !important;
|
||||
}
|
||||
|
||||
.ant-tabs-ink-bar {
|
||||
background-color: #cca6ff;
|
||||
}
|
||||
|
||||
.ant-tabs-tab {
|
||||
color: #b485f1;
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@
|
||||
"name": "Contributing",
|
||||
"items": [
|
||||
"docs/contributing",
|
||||
"docs/recognizing-contributions",
|
||||
"docs/developing-locally",
|
||||
"docs/stack",
|
||||
"docs/project-structure",
|
||||
|
||||
10
src/types.ts
10
src/types.ts
@@ -41,3 +41,13 @@ export interface MenuQueryType {
|
||||
edges: MenuQueryNodeType[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface Contributor {
|
||||
login: string
|
||||
name: string
|
||||
avatar_url: string
|
||||
profile: string
|
||||
contributions: string[]
|
||||
level: number
|
||||
mvpWins: number
|
||||
}
|
||||
|
||||
34
yarn.lock
34
yarn.lock
@@ -1760,6 +1760,13 @@
|
||||
"@types/node" "*"
|
||||
"@types/responselike" "*"
|
||||
|
||||
"@types/chart.js@^2.9.31":
|
||||
version "2.9.31"
|
||||
resolved "https://registry.yarnpkg.com/@types/chart.js/-/chart.js-2.9.31.tgz#e8ebc7ed18eb0e5114c69bd46ef8e0037c89d39d"
|
||||
integrity sha512-hzS6phN/kx3jClk3iYqEHNnYIRSi4RZrIGJ8CDLjgatpHoftCezvC44uqB3o3OUm9ftU1m7sHG8+RLyPTlACrA==
|
||||
dependencies:
|
||||
moment "^2.10.2"
|
||||
|
||||
"@types/common-tags@^1.8.0":
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/common-tags/-/common-tags-1.8.0.tgz#79d55e748d730b997be5b7fce4b74488d8b26a6b"
|
||||
@@ -3863,6 +3870,29 @@ chardet@^0.7.0:
|
||||
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
|
||||
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
|
||||
|
||||
chart.js@^2.9.4:
|
||||
version "2.9.4"
|
||||
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.9.4.tgz#0827f9563faffb2dc5c06562f8eb10337d5b9684"
|
||||
integrity sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==
|
||||
dependencies:
|
||||
chartjs-color "^2.1.0"
|
||||
moment "^2.10.2"
|
||||
|
||||
chartjs-color-string@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz#1df096621c0e70720a64f4135ea171d051402f71"
|
||||
integrity sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==
|
||||
dependencies:
|
||||
color-name "^1.0.0"
|
||||
|
||||
chartjs-color@^2.1.0:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/chartjs-color/-/chartjs-color-2.4.1.tgz#6118bba202fe1ea79dd7f7c0f9da93467296c3b0"
|
||||
integrity sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==
|
||||
dependencies:
|
||||
chartjs-color-string "^0.6.0"
|
||||
color-convert "^1.9.3"
|
||||
|
||||
cheerio-select-tmp@^0.1.0:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/cheerio-select-tmp/-/cheerio-select-tmp-0.1.1.tgz#55bbef02a4771710195ad736d5e346763ca4e646"
|
||||
@@ -4113,7 +4143,7 @@ collection-visit@^1.0.0:
|
||||
map-visit "^1.0.0"
|
||||
object-visit "^1.0.0"
|
||||
|
||||
color-convert@^1.9.0, color-convert@^1.9.1:
|
||||
color-convert@^1.9.0, color-convert@^1.9.1, color-convert@^1.9.3:
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
|
||||
@@ -10977,7 +11007,7 @@ modern-normalize@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/modern-normalize/-/modern-normalize-1.0.0.tgz#539d84a1e141338b01b346f3e27396d0ed17601e"
|
||||
integrity sha512-1lM+BMLGuDfsdwf3rsgBSrxJwAZHFIrQ8YR61xIqdHo0uNKI9M52wNpHSrliZATJp51On6JD0AfRxd4YGSU0lw==
|
||||
|
||||
moment@2.x, moment@^2.24.0, moment@^2.27.0:
|
||||
moment@2.x, moment@^2.10.2, moment@^2.24.0, moment@^2.27.0:
|
||||
version "2.29.1"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
|
||||
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
|
||||
|
||||
Reference in New Issue
Block a user