[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:
Yakko Majuri
2021-03-30 13:27:10 +00:00
committed by GitHub
parent ea3d44025e
commit a36b736ea8
21 changed files with 936 additions and 55 deletions

View 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.

View File

@@ -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
View 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)

View File

@@ -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']

View 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',
},
}

View 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>
)
}

View 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;
}
}

View 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)}
/>
)
}

View 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;
}
}

View 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()
}
},
}),
})

View 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" />
</>
)}
</>
)
}

View File

@@ -1594,3 +1594,9 @@ div::-webkit-scrollbar-thumb:hover {
.text-center {
text-align: center;
}
.contributor-card-tooltip {
.ant-tooltip-inner {
background-color: #0f041f;
}
}

View File

@@ -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 />

View 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()
}
},
}),
})

View File

@@ -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
}

View 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,
}

View 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

View 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;
}
}

View File

@@ -33,6 +33,7 @@
"name": "Contributing",
"items": [
"docs/contributing",
"docs/recognizing-contributions",
"docs/developing-locally",
"docs/stack",
"docs/project-structure",

View File

@@ -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
}

View File

@@ -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==