├── public ├── CNAME ├── og.png ├── robots.txt ├── manifest.json └── index.html ├── src ├── submit │ ├── issue.css │ ├── Button.tsx │ ├── index.tsx │ ├── Find.tsx │ ├── Preview.tsx │ ├── button.css │ └── Issue.tsx ├── rfm │ ├── services │ │ ├── analytics │ │ │ └── index.ts │ │ ├── github │ │ │ └── index.ts │ │ ├── api │ │ │ └── github.ts │ │ └── sw │ │ │ └── index.ts │ └── components │ │ ├── Circle │ │ └── index.tsx │ │ ├── Shell │ │ ├── index.tsx │ │ ├── Header.tsx │ │ └── Footer.tsx │ │ ├── Error │ │ └── index.tsx │ │ ├── Progress │ │ └── index.tsx │ │ ├── Star │ │ └── index.tsx │ │ ├── Comments │ │ └── index.tsx │ │ ├── Issue │ │ └── index.tsx │ │ └── Info │ │ └── index.tsx ├── setupTests.ts ├── home │ ├── Home.spec.tsx │ ├── index.tsx │ ├── Search.tsx │ ├── NewsletterBanner.tsx │ ├── List.tsx │ ├── Newsletter.tsx │ └── newsletter.css ├── react-app-env.d.ts ├── confirm │ └── index.tsx ├── tailwind.src.css ├── index.tsx └── about │ ├── index.tsx │ └── markdown.css ├── craco.config.js ├── tailwind.config.js ├── .prettierrc ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ └── rfm.md └── workflows │ ├── release.yml │ └── issue.yml ├── postcss.config.js ├── .gitignore ├── tsconfig.json ├── LICENSE ├── README.md └── package.json /public/CNAME: -------------------------------------------------------------------------------- 1 | rfm.sospedra.me 2 | -------------------------------------------------------------------------------- /public/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sospedra/rfm/HEAD/public/og.png -------------------------------------------------------------------------------- /src/submit/issue.css: -------------------------------------------------------------------------------- 1 | .markdown > div, 2 | .markdown p { 3 | display: inline; 4 | } 5 | -------------------------------------------------------------------------------- /craco.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | eslint: { 3 | enable: false, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | theme: { 3 | extend: {}, 4 | }, 5 | variants: {}, 6 | plugins: [], 7 | } 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "endOfLine": "lf", 4 | "jsxSingleQuote": true, 5 | "semi": false, 6 | "singleQuote": true, 7 | "trailingComma": "all" 8 | } 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: 🚧 Submit a new RFM 3 | url: https://sospedra.github.io/rfm/#/submit 4 | about: Add a new repo that needs a maintainer 5 | -------------------------------------------------------------------------------- /src/rfm/services/analytics/index.ts: -------------------------------------------------------------------------------- 1 | import mixpanel from 'mixpanel-browser' 2 | 3 | mixpanel.init('e584fa40e066890465612b19042dddd1') 4 | 5 | export const track: typeof mixpanel.track = (...args) => { 6 | mixpanel.track(...args) 7 | } 8 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "rfm", 3 | "name": "Request for maintainers", 4 | "icons": [], 5 | "start_url": ".", 6 | "display": "standalone", 7 | "theme_color": "#000000", 8 | "background_color": "#ffffff" 9 | } 10 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect' 6 | -------------------------------------------------------------------------------- /src/rfm/components/Circle/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Circle: React.FC<{ color?: string }> = (props) => ( 4 | 12 | ) 13 | 14 | export default Circle 15 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const purgecss = require('@fullhuman/postcss-purgecss')({ 2 | content: ['./src/**/*.tsx', './src/**/*.ts', './public/index.html'], 3 | defaultExtractor: (content) => content.match(/[\w-/:]+(? = (props) => ( 6 |
7 |
8 |
9 | {props.children} 10 |
11 |
12 |
13 | ) 14 | 15 | export default Shell 16 | -------------------------------------------------------------------------------- /src/rfm/components/Error/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Error: React.FC<{ 4 | error: any 5 | }> = (props) => { 6 | if (!props.error) return null 7 | return ( 8 | 12 | ) 13 | } 14 | 15 | export default Error 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | /src/tailwind.css 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/rfm.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '(Discouraged) Manual submit' 3 | about: Manually add a new repo that needs a maintainer. Preferred way is to use the web 4 | title: '{owner}/{name}' 5 | labels: 'search' 6 | --- 7 | 8 | { 9 | "description": "", 10 | "fullName": "", 11 | "language": "", 12 | "license": "", 13 | "name": "", 14 | "owner": "", 15 | "openIssues": 0, 16 | "stars": 0, 17 | "topics": "", 18 | "updatedAt": "", 19 | "url": "" 20 | } 21 | -------------------------------------------------------------------------------- /src/home/Home.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from '@testing-library/react' 3 | import { HashRouter } from 'react-router-dom' 4 | import Home from './index' 5 | 6 | test('renders learn react link', () => { 7 | const { getByText } = render( 8 | 9 | 10 | , 11 | ) 12 | const linkElement = getByText(/Browse repos that need support/i) 13 | expect(linkElement).toBeInTheDocument() 14 | }) 15 | -------------------------------------------------------------------------------- /src/rfm/components/Progress/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useSpring, animated } from 'react-spring' 3 | 4 | const Progress: React.FC<{ 5 | ratio: number 6 | }> = (props) => { 7 | const style = useSpring({ width: props.ratio * document.body.clientWidth }) 8 | 9 | return ( 10 |
11 | 12 |
13 | ) 14 | } 15 | 16 | export default Progress 17 | -------------------------------------------------------------------------------- /src/rfm/components/Star/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Star: React.FC<{}> = () => ( 4 | 13 | 17 | 18 | ) 19 | 20 | export default Star 21 | -------------------------------------------------------------------------------- /src/rfm/components/Comments/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Comments: React.FC<{}> = () => { 4 | return ( 5 | 18 | ) 19 | } 20 | 21 | export default Comments 22 | -------------------------------------------------------------------------------- /src/rfm/components/Issue/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Issue: React.FC<{}> = () => ( 4 | 17 | ) 18 | 19 | export default Issue 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /src/rfm/components/Shell/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router-dom' 3 | 4 | const Header: React.FC<{}> = () => { 5 | return ( 6 |
7 | 8 | 9 | 🚧 10 | {' '} 11 | rfm 12 | 13 | 17 | Submit new repo 18 | 19 |
20 | ) 21 | } 22 | 23 | export default Header 24 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module 'language-map' { 4 | const d: { 5 | [name: string]: 6 | | { 7 | type: 'data' | 'programming' | 'markup' | undefined 8 | aliases: string[] 9 | filenames: string[] 10 | extensions: string[] 11 | interpreters: string[] 12 | wrap: boolean 13 | color: string 14 | group: string 15 | aceMode: string 16 | searchable: string 17 | searchTerm: string 18 | languageId: number 19 | } 20 | | undefined 21 | } 22 | export default d 23 | } 24 | 25 | declare module 'human-number' { 26 | const d: (n: number) => string 27 | export default d 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | release: 8 | if: "!contains(github.event.head_commit.message, 'skip ci')" 9 | name: Release 10 | runs-on: ubuntu-18.04 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v1 14 | - name: Setup Node.js 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: 13 18 | - name: Install dependencies 19 | run: yarn install 20 | - name: Publish github pages 21 | run: | 22 | git remote set-url origin https://git:${GITHUB_TOKEN}@github.com/sospedra/rfm.git 23 | yarn deploy 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /src/confirm/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Shell from '../rfm/components/Shell' 3 | import { Link } from 'react-router-dom' 4 | 5 | const Confirm: React.FC<{}> = (props) => { 6 | return ( 7 | 8 |
9 |

Almost there!

10 |

Your privacy means a lot to us.

11 |

12 | That's why you will need to{' '} 13 | check your inbox and confirm your subscription. 14 |

15 |

Thanks for subscribing.

16 | 17 | 21 | Take me back to the home 22 | 23 |
24 |
25 | ) 26 | } 27 | 28 | export default Confirm 29 | -------------------------------------------------------------------------------- /src/tailwind.src.css: -------------------------------------------------------------------------------- 1 | /* purgecss start ignore */ 2 | @tailwind base; 3 | @tailwind components; 4 | /* purgecss end ignore */ 5 | @tailwind utilities; 6 | html { 7 | scroll-behavior: smooth; 8 | } 9 | html, 10 | body { 11 | @apply overflow-x-hidden; 12 | display: contents; 13 | } 14 | a.link { 15 | @apply text-blue-600; 16 | } 17 | a.link:hover { 18 | @apply text-blue-800; 19 | } 20 | a[href*="//"]:not([href*="sospedra.me"]), 21 | a[href*="//"]:not([href*="rfm.sospedra.me"]) 22 | { 23 | display: inline-flex; 24 | position: relative; 25 | } 26 | a[href*="//"]:not([href*="sospedra.me"]):after, 27 | a[href*="//"]:not([href*="rfm.sospedra.me"]):after 28 | { 29 | @apply inline-block opacity-75; 30 | content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAQElEQVR42qXKwQkAIAxDUUdxtO6/RBQkQZvSi8I/pL4BoGw/XPkh4XigPmsUgh0626AjRsgxHTkUThsG2T/sIlzdTsp52kSS1wAAAABJRU5ErkJggg==); 31 | transform: translateY(-3px) scale(0.8); 32 | } 33 | -------------------------------------------------------------------------------- /src/rfm/components/Info/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const Info: React.FC<{}> = () => { 4 | return ( 5 | 18 | ) 19 | } 20 | 21 | export default Info 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Rubén Sospedra 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/submit/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './button.css' 3 | 4 | const Button: React.FC<{ 5 | disabled?: boolean 6 | href?: string 7 | form?: string 8 | loading?: boolean 9 | onClick?: () => any 10 | children: string 11 | }> = (props) => { 12 | return props.href ? ( 13 | 19 | {props.children} 20 | 21 | ) : ( 22 |
29 | 41 |
42 | ) 43 | } 44 | 45 | export default Button 46 | -------------------------------------------------------------------------------- /src/rfm/components/Shell/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link } from 'react-router-dom' 3 | 4 | const Footer: React.FC<{}> = () => { 5 | return ( 6 | 45 | ) 46 | } 47 | 48 | export default Footer 49 | -------------------------------------------------------------------------------- /src/rfm/services/github/index.ts: -------------------------------------------------------------------------------- 1 | import newGithubIssueUrl from 'new-github-issue-url' 2 | import isURL from 'validator/lib/isURL' 3 | import { SubmitRequest } from '../api/github' 4 | 5 | export const createGithubIssue = (request?: SubmitRequest) => { 6 | if (!request) return '' 7 | return newGithubIssueUrl({ 8 | body: JSON.stringify(request, null, 4), 9 | labels: ['search'], 10 | repo: 'rfm', 11 | title: request.fullName, 12 | user: 'sospedra', 13 | }) 14 | } 15 | 16 | export const isValidGithubUrl = (candidate: string) => { 17 | try { 18 | const isValidUrl = isURL(candidate, { 19 | host_whitelist: [/^.*github\.com$/], 20 | }) 21 | const isValidFullName = 22 | candidate.split('/').length === 2 && candidate.match(/\//gi)?.length === 1 23 | 24 | return isValidUrl || isValidFullName 25 | } catch (ex) { 26 | return false 27 | } 28 | } 29 | 30 | export const createGithubRepoUrl = (candidate: string) => { 31 | return isURL(candidate) ? candidate : `https://github.com/${candidate}` 32 | } 33 | 34 | export const NONE_ISSUE = 'NONE' 35 | 36 | export const isValidGithubIssueUrl = (candidate: string) => { 37 | try { 38 | if (candidate === NONE_ISSUE) return true 39 | 40 | if (isValidGithubUrl(candidate)) { 41 | const [path, number] = candidate.split('/').slice(-2) 42 | if (path === 'issues') { 43 | if (!Number.isNaN(Number(number))) { 44 | return true 45 | } 46 | } 47 | } 48 | 49 | return false 50 | } catch (ex) { 51 | return false 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/home/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import useSWR from 'swr' 3 | import { fetcherRequestList } from '../rfm/services/api/github' 4 | import Shell from '../rfm/components/Shell' 5 | import Error from '../rfm/components/Error' 6 | import { track } from '../rfm/services/analytics' 7 | import Search from './Search' 8 | import List from './List' 9 | import Newsletter from './Newsletter' 10 | 11 | const Home: React.FC<{}> = () => { 12 | const [query, setQuery] = useState(' ') 13 | const { data, error } = useSWR(query, fetcherRequestList) 14 | 15 | useEffect(() => { 16 | if (query !== ' ') { 17 | track('search', { query, total: data?.total || 0 }) 18 | } 19 | }, [query]) 20 | 21 | return ( 22 | 23 | 24 | 29 | Request for maintainers - Find any OSS project calling for collaborators | Product Hunt Embed 36 | 37 | 38 | 39 | 40 | 41 | ) 42 | } 43 | 44 | export default Home 45 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { useTransition, animated } from 'react-spring' 4 | import { HashRouter, Route, useLocation, Switch } from 'react-router-dom' 5 | import * as serviceWorker from './rfm/services/sw' 6 | import { track } from './rfm/services/analytics' 7 | import './tailwind.css' 8 | import Home from './home' 9 | import Submit from './submit' 10 | import Confirm from './confirm' 11 | import About from './about' 12 | 13 | const App: React.FC<{}> = () => { 14 | const location = useLocation() 15 | const transitions = useTransition(location, (location) => location.pathname, { 16 | from: { opacity: 0 }, 17 | enter: { opacity: 1 }, 18 | leave: { opacity: 0 }, 19 | }) 20 | 21 | useEffect(() => { 22 | track('pview', { route: location.pathname }) 23 | }, [location]) 24 | 25 | return ( 26 | <> 27 | {transitions.map(({ item: location, props, key }) => ( 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ))} 37 | 38 | ) 39 | } 40 | 41 | ReactDOM.render( 42 | 43 | 44 | 45 | 46 | , 47 | document.getElementById('root'), 48 | ) 49 | 50 | serviceWorker.unregister() 51 | -------------------------------------------------------------------------------- /src/home/Search.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Info from '../rfm/components/Info' 3 | import { Link } from 'react-router-dom' 4 | 5 | const Search: React.FC<{ 6 | setQuery: (query: string) => void 7 | }> = (props) => { 8 | return ( 9 |
10 |

11 | 12 | Track OSS requests for maintainers 13 | 14 |

15 |
{ 18 | e.preventDefault() 19 | const form = new FormData(e.currentTarget) 20 | const query = form.get('search') as string 21 | props.setQuery(query) 22 | }} 23 | > 24 | 29 |
30 | 36 | 41 |
42 |
43 |
44 | ) 45 | } 46 | 47 | export default Search 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚧 RFM | Request for maintainers 2 | 3 | ## _Track OSS requests for maintainers_ 4 | 5 | [![Request for maintainers - Find any OSS project calling for collaborators | Product Hunt Embed](https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=195531&theme=dark)](https://www.producthunt.com/posts/request-for-maintainers?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-request-for-maintainers) 6 | 7 | ### What's this? 8 | 9 | RFM is community-driven platform to **track OSS repositories that need a maintainer** or support. 10 | 11 | ### Why? 12 | 13 | You're interested in this project if you've been in any of these situations: 14 | 15 | - As a **user**, uou find and interesting library. But it seems unmaintained. How to know for sure? 16 | - As a **maintainer**, you can't find anyone who wants to take the lead. Where to find them? 17 | - As a **developer**, you want to contribute to the community but don't know where to start. Which projects need help? 18 | 19 | ### How does it work? 20 | 21 | It heavily relies on the Github public API (which is awesome). 22 | 23 | 1. Every request is an Issue labeled as `search` in this repository. 24 | 2. The body of the issue contains a JSON with the searchable data. 25 | 3. It uses the Github Search API to find tickets. 26 | 4. To avoid undesired format errors the web has a request genertor, as well. 27 | 28 | These are the main steps. Aside of it, RFM also checks that nobody use the platform as a spam weapon, checks for duplicates, ensures the data integrity, etc. 29 | 30 | ### Contribute 31 | 32 | 1. 🤗 [PRs](https://github.com/sospedra/rfm) are more than welcome 33 | 2. 🕵🏽‍♀️ [Add](https://rfm.sospedra.me/#/submit) any repo you find that's unmaintained 34 | 3. 🌎 Spread the word 35 | 4. Thank you! 36 | 37 | _Hand-crafted with 💜 by [@sospedra](https://sospedra.me)_ 38 | -------------------------------------------------------------------------------- /.github/workflows/issue.yml: -------------------------------------------------------------------------------- 1 | name: Create external issues 2 | on: 3 | issues: 4 | types: [labeled] 5 | 6 | jobs: 7 | create: 8 | if: "contains(github.event.issue.labels.*.name, 'search')" 9 | name: Create issue 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: vars 13 | id: vars 14 | uses: gr2m/get-json-paths-action@v1.x 15 | with: 16 | json: ${{ github.event.issue.body }} 17 | owner: 'owner' 18 | name: 'name' 19 | requestIssueFullName: 'requestIssueFullName' 20 | requestIssueNumber: 'requestIssueNumber' 21 | - name: create-issue 22 | if: steps.vars.outputs.requestIssueFullName == 'NONE' 23 | uses: maxkomarychev/oction-create-issue@v0.7.1 24 | with: 25 | token: ${{ secrets.RFM_BOT }} 26 | title: 🚧 Is this repo looking for support? 27 | owner: ${{ steps.vars.outputs.owner }} 28 | repo: ${{ steps.vars.outputs.name }} 29 | body: | 30 | Hello, we created this issue becuase the user @${{ github.event.issue.user.login }} told us you are calling for maintainers. 31 | ✅ If you're **looking for collaborators** no action is required. 32 | 👮🏻‍♂️ If this repo is **well-supported please put a comment** here ${{ github.event.issue.html_url }} and we'll close it immediately. 33 | Sorry for any inconvinience. We understand this message can feel spammy but we really think is good to double-check first with the current owners :) 34 | - name: comment-issue 35 | if: steps.vars.outputs.requestIssueFullName != 'NONE' 36 | uses: peter-evans/create-or-update-comment@v1 37 | with: 38 | token: ${{ secrets.RFM_BOT }} 39 | issue-number: ${{ steps.vars.outputs.requestIssueNumber }} 40 | repository: ${{ steps.vars.outputs.requestIssueFullName }} 41 | body: | 42 | 🚧 Is this repo looking for support? 43 | Hello, we created this issue becuase the user @${{ github.event.issue.user.login }} told us you are calling for maintainers. 44 | ✅ If you're **looking for collaborators** no action is required. 45 | 👮🏻‍♂️ If this repo is **well-supported please put a comment** here ${{ github.event.issue.html_url }} and we'll close it immediately. 46 | Sorry for any inconvinience. We understand this message can feel spammy but we really think is good to double-check first with the current owners :) 47 | -------------------------------------------------------------------------------- /src/submit/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from 'react' 2 | import useSWR from 'swr' 3 | import { useTransition, animated } from 'react-spring' 4 | import { fetcherSubmitRequest } from '../rfm/services/api/github' 5 | import Shell from '../rfm/components/Shell' 6 | import Progress from '../rfm/components/Progress' 7 | import Preview from './Preview' 8 | import Issue from './Issue' 9 | import Find from './Find' 10 | 11 | const Submit: React.FC<{}> = () => { 12 | const [repoUrl, setRepoUrl] = useState('') 13 | const [requestIssue, setRequestIssue] = useState('') 14 | const { data, error } = useSWR(repoUrl, fetcherSubmitRequest) 15 | const [index, setIndex] = useState(0) 16 | const onNext = useCallback(() => setIndex((state) => (state + 1) % 3), []) 17 | const transitions = useTransition(index, (p) => p, { 18 | from: { 19 | display: 'block', 20 | width: '100%', 21 | opacity: 0, 22 | transform: 'translate3d(100%, 0, 0)', 23 | }, 24 | enter: { 25 | display: 'block', 26 | width: '100%', 27 | opacity: 1, 28 | transform: 'translate3d(0%, 0, 0)', 29 | }, 30 | leave: { 31 | display: 'none', 32 | width: '100%', 33 | opacity: 0, 34 | transform: 'translate3d(-50%, 0, 0)', 35 | }, 36 | }) 37 | 38 | return ( 39 | 40 |
41 | 42 |
43 | {transitions.map(({ item, props, key }) => { 44 | return [ 45 | 46 | 52 | , 53 | 54 | 60 | , 61 | 62 | 63 | , 64 | ][item] 65 | })} 66 |
67 |
68 |
69 | ) 70 | } 71 | 72 | export default Submit 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rfm", 3 | "version": "0.1.1", 4 | "homepage": "https://rfm.sospedra.me", 5 | "private": false, 6 | "license": "SEE LICENSE IN LICENSE", 7 | "eslintConfig": { 8 | "extends": "react-app" 9 | }, 10 | "browserslist": { 11 | "production": [ 12 | ">0.2%", 13 | "not dead", 14 | "not op_mini all" 15 | ], 16 | "development": [ 17 | "last 1 chrome version", 18 | "last 1 firefox version", 19 | "last 1 safari version" 20 | ] 21 | }, 22 | "husky": { 23 | "hooks": { 24 | "pre-commit": "pretty-quick --staged" 25 | } 26 | }, 27 | "scripts": { 28 | "build:css": "postcss src/tailwind.src.css -o src/tailwind.css --env production", 29 | "build:js": "craco build", 30 | "build": "run-s build:*", 31 | "deploy:build": "yarn build", 32 | "deploy:release": "gh-pages -d build -u \"github-actions-bot \"", 33 | "deploy": "run-s deploy:*", 34 | "start:css": "postcss src/tailwind.src.css -o src/tailwind.css -w", 35 | "start:js": "craco start", 36 | "start": "run-p start:*", 37 | "test": "craco test" 38 | }, 39 | "dependencies": { 40 | "@testing-library/jest-dom": "^4.2.4", 41 | "@testing-library/react": "^9.3.2", 42 | "@testing-library/user-event": "^7.1.2", 43 | "@types/jest": "^24.0.0", 44 | "@types/node": "^12.0.0", 45 | "@types/react": "^16.9.0", 46 | "@types/react-dom": "^16.9.0", 47 | "human-number": "^1.0.5", 48 | "language-map": "^1.4.0", 49 | "markdown-to-jsx": "^6.11.1", 50 | "mixpanel-browser": "^2.35.0", 51 | "new-github-issue-url": "^0.2.1", 52 | "react": "^16.13.1", 53 | "react-content-loader": "^5.0.4", 54 | "react-dom": "^16.13.1", 55 | "react-router-dom": "^5.1.2", 56 | "react-scripts": "3.4.1", 57 | "react-spring": "^8.0.27", 58 | "resize-observer-polyfill": "^1.5.1", 59 | "swr": "^0.2.0", 60 | "tailwindcss": "^1.2.0", 61 | "typescript": "~3.7.2", 62 | "validator": "^13.0.0" 63 | }, 64 | "devDependencies": { 65 | "@craco/craco": "^5.6.4", 66 | "@fullhuman/postcss-purgecss": "^2.1.2", 67 | "@types/markdown-to-jsx": "^6.11.0", 68 | "@types/mixpanel-browser": "^2.35.0", 69 | "@types/react-router-dom": "^5.1.4", 70 | "@types/validator": "^13.0.0", 71 | "gh-pages": "^2.2.0", 72 | "husky": "^4.2.5", 73 | "npm-run-all": "^4.1.5", 74 | "postcss-cli": "^7.1.0", 75 | "prettier": "^2.0.4", 76 | "pretty-quick": "^2.0.1", 77 | "purgecss": "^2.1.2" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/submit/Find.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import { createGithubRepoUrl, isValidGithubUrl } from '../rfm/services/github' 3 | import { SubmitRequest } from '../rfm/services/api/github' 4 | import Error from '../rfm/components/Error' 5 | import Button from './Button' 6 | import { track } from '../rfm/services/analytics' 7 | 8 | const Repo: React.FC<{ 9 | setRepoUrl: (repo: string) => void 10 | onNext: () => void 11 | error: any 12 | data?: SubmitRequest 13 | }> = (props) => { 14 | const [inputValue, setInputValue] = useState('') 15 | const [loading, setLoading] = useState(false) 16 | 17 | useEffect(() => { 18 | if (props.data?.fullName) { 19 | track('submit', { step: 'find' }) 20 | props.onNext() 21 | } else { 22 | setLoading(false) 23 | } 24 | }, [props.data]) 25 | 26 | return ( 27 |
28 |

29 | Add a new repository that needs maintance 30 |

31 | 32 |
{ 35 | e.preventDefault() 36 | setLoading(true) 37 | props.setRepoUrl(createGithubRepoUrl(inputValue)) 38 | }} 39 | > 40 |
41 | setInputValue(e.currentTarget.value)} 44 | className='w-full py-2 pl-24 pr-4 my-4 border rounded shadow-lg' 45 | required 46 | /> 47 | 55 | github.com/ 56 | 57 |
58 | 59 | 60 | {props.data && !props.data?.fullName && ( 61 |
62 |

63 | We couldn't find any repo named{' '} 64 | 65 | {props.data?.owner}/{props.data?.name} 66 | 67 |

68 |

Try to copy and paste the link directly

69 |
70 | )} 71 | 74 | 75 |
76 | ) 77 | } 78 | 79 | export default Repo 80 | -------------------------------------------------------------------------------- /src/about/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Markdown from 'markdown-to-jsx' 3 | import Shell from '../rfm/components/Shell' 4 | import './markdown.css' 5 | 6 | const md = ` 7 | ### What's this? 8 | 9 | RFM is community-driven platform to **track OSS repositories that need a maintainer** or support. 10 | 11 | ### Why? 12 | 13 | You're interested in this project if you've been in any of these situations: 14 | 15 | - As a **user**, you find and interesting library. But it seems unmaintained. How to know for sure? 16 | - As a **maintainer**, you can't find anyone who wants to take the lead. Where to find them? 17 | - As a **developer**, you want to contribute to the community but don't know where to start. Which projects need help? 18 | 19 | ### How does it work? 20 | 21 | It heavily relies on the Github public API (which is awesome). 22 | 23 | 1. Every request is an Issue labeled as \`search\` in this repository. 24 | 2. The body of the issue contains a JSON with the searchable data. 25 | 3. It uses the Github Search API to find tickets. 26 | 4. To avoid undesired format errors the web has a request genertor, as well. 27 | 28 | These are the main steps. Aside of it, RFM also checks that nobody use the platform as a spam weapon, checks for duplicates, ensures the data integrity, etc. 29 | 30 | ### Contribute 31 | 32 | 1. 🤗 [PRs](https://github.com/sospedra/rfm) are more than welcome 33 | 2. 🕵🏽‍♀️ [Add](https://rfm.sospedra.me/#/submit) any repo you find that's unmaintained 34 | 3. 🌎 Spread the word 35 | 4. Thank you! 36 | ` 37 | 38 | const About: React.FC<{}> = (props) => { 39 | return ( 40 | 41 |

About RFM

42 |

43 | Track OSS requests for maintainers 44 |

45 | 50 | Request for maintainers - Find any OSS project calling for collaborators | Product Hunt Embed 57 | 58 |
59 | {md} 60 |
61 |
62 | ) 63 | } 64 | 65 | export default About 66 | -------------------------------------------------------------------------------- /src/submit/Preview.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { SubmitRequest } from '../rfm/services/api/github' 3 | import { createGithubIssue } from '../rfm/services/github' 4 | import Button from './Button' 5 | import { track } from '../rfm/services/analytics' 6 | 7 | const formatProperty = (key: string, data?: SubmitRequest) => { 8 | const property = data && data[key as keyof SubmitRequest] 9 | if (property instanceof Array) return `[${property.join(', ')}]` 10 | return property 11 | } 12 | 13 | const parseRequestIssue = (requestIssue: string) => { 14 | if (requestIssue === 'NONE') 15 | return { 16 | requestIssueURL: requestIssue, 17 | requestIssueNumber: -1, 18 | } 19 | 20 | const [left, number] = requestIssue.split('/issues/') 21 | return { 22 | requestIssueFullName: left.split('/').slice(-2).join('/'), 23 | requestIssueNumber: Number(number), 24 | } 25 | } 26 | 27 | const Preview: React.FC<{ 28 | data?: SubmitRequest 29 | requestIssue: string 30 | }> = (props) => { 31 | const repo = { 32 | ...props.data, 33 | ...parseRequestIssue(props.requestIssue), 34 | } as SubmitRequest 35 | return ( 36 |
37 |

Save the request

38 |

39 | You're gonna be redirected to our Github to save this request. After 40 | that the project {repo?.fullName} will be marked as calling for 41 | maintainers. 42 |

43 | 49 |
50 |

51 | Inspect 52 |

53 |

This is the repo you're about to submit:

54 | 55 |
56 |           

{`{`}

57 | {Object.keys(repo || {}).map((key) => ( 58 |

59 | {key} 60 | 61 | : 62 | 63 | {' '} 64 | {formatProperty(key, repo)} 65 | 66 | , 67 | 68 |

69 | ))} 70 |

{`}`}

71 |
72 |
73 |
74 | ) 75 | } 76 | 77 | export default Preview 78 | -------------------------------------------------------------------------------- /src/submit/button.css: -------------------------------------------------------------------------------- 1 | #submit { 2 | display: block; 3 | } 4 | #submit:after { 5 | filter: invert(1); 6 | } 7 | .gradient { 8 | --offset: 3px; 9 | --duration: 4s; 10 | position: relative; 11 | padding: var(--offset); 12 | z-index: 0; 13 | overflow: hidden; 14 | border-radius: 3px; 15 | } 16 | .gradient.loading::before, 17 | .gradient.loading::after { 18 | content: ''; 19 | position: absolute; 20 | background-color: aqua; 21 | z-index: -1; 22 | top: 0; 23 | left: 0; 24 | right: 0; 25 | bottom: 0; 26 | } 27 | 28 | .gradient.loading::before { 29 | --skew-x: 11.5deg; 30 | --offset-x: -9px; 31 | --offset-y: -3px; 32 | animation: bef var(--duration) linear infinite; 33 | transform-origin: left top; 34 | transform: translateX(-100%) translateY(-100%) translateY(3px); 35 | } 36 | 37 | .gradient.loading::after { 38 | --skew-x: 11.5deg; 39 | --offset-x: -9px; 40 | --offset-y: -3px; 41 | animation: aft var(--duration) linear infinite; 42 | transform-origin: left top; 43 | transform: skewX(var(--skew-x)) translateX(100%) translateY(100%) 44 | translateY(-3px); 45 | } 46 | 47 | @keyframes bef { 48 | 0% /* 0s */ { 49 | transform: translateX(-100%) translateY(-100%) translateY(3px); 50 | } 51 | 21.6% /* 1s */ { 52 | transform: translateX(3px) translateY(-100%) translateY(3px); 53 | } 54 | 25% /* 2s */ { 55 | transform: translateX(3px) translateY(-3px); 56 | } 57 | 46.6% /* 3s */ { 58 | } 59 | 50% /* 4s */ { 60 | transform: translateX(3px) translateY(-3px); 61 | } 62 | 71.6% /* 5s */ { 63 | transform: translateX(100%) translateX(-3px) translateY(-3px); 64 | } 65 | 75% /* 6s */ { 66 | transform: translateX(100%) translateX(-3px) translateY(100%); 67 | } 68 | 95.6% /* 7s */ { 69 | transform: translateX(100%) translateX(-3px) translateY(100%); 70 | } 71 | 100% /* 8s */ { 72 | transform: translateX(100%) translateX(-3px) translateY(100%); 73 | } 74 | } 75 | 76 | @keyframes aft { 77 | 0% /* 0s */ { 78 | transform: skewX(var(--skew-x)) translateX(100%) translateY(100%) 79 | translateY(var(--offset-y)); 80 | } 81 | 21.6% /* 1s */ { 82 | } 83 | 25% /* 2s */ { 84 | transform: skewX(var(--skew-x)) translateX(100%) translateY(100%) 85 | translateY(var(--offset-y)); 86 | } 87 | 46.6% /* 3s */ { 88 | transform: skewX(var(--skew-x)) translateX(var(--offset-x)) translateY(100%) 89 | translateY(var(--offset-y)); 90 | } 91 | 50% /* 4s */ { 92 | transform: skewX(var(--skew-x)) translateX(var(--offset-x)) 93 | translateY(var(--offset)); 94 | } 95 | 71.6% /* 5s */ { 96 | } 97 | 75% /* 6s */ { 98 | --skew-x: 11.5deg; 99 | transform: skewX(var(--skew-x)) translateX(var(--offset-x)) 100 | translateY(var(--offset)); 101 | } 102 | 95.6% /* 7s */ { 103 | transform: skewX(var(--skew-x)) translateX(-100%) translateX(var(--offset)) 104 | translateY(var(--offset)); 105 | } 106 | 100% /* 8s */ { 107 | --skew-x: 0deg; 108 | transform: skewX(var(--skew-x)) translateX(-100%) translateX(var(--offset)) 109 | translateY(var(--offset)) scaleY(0); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/home/NewsletterBanner.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { useSpring, animated } from 'react-spring' 3 | 4 | const NewsletterBanner: React.FC<{}> = () => { 5 | const [isBeating, setIsBeating] = useState(false) 6 | const { x } = useSpring({ 7 | from: { x: 0 }, 8 | x: isBeating ? 1 : 0, 9 | config: { duration: 1000 }, 10 | }) 11 | 12 | return ( 13 | 81 | ) 82 | } 83 | 84 | export default NewsletterBanner 85 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 16 | 20 | 21 | 30 | 31 | 32 | RFM — Request for maintainers 33 | 34 | 38 | 39 | 40 | 41 | 42 | 43 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 57 | 58 | 59 | 60 | 61 | 62 |
63 | 73 | 74 | 75 | 79 | 88 | 89 | -------------------------------------------------------------------------------- /src/rfm/services/api/github.ts: -------------------------------------------------------------------------------- 1 | import langmap from 'language-map' 2 | 3 | const GITHUB_ROOT = 'https://api.github.com' 4 | 5 | export type SubmitRequest = { 6 | aceMode?: string 7 | aliases?: string[] 8 | color?: string 9 | description: string 10 | extensions?: string[] 11 | filenames?: string[] 12 | fullName: string 13 | owner: string 14 | name: string 15 | group?: string 16 | interpreters?: string[] 17 | language: string 18 | license?: string 19 | requestIssueFullName: string 20 | requestIssueNumber: number 21 | openIssues: number 22 | stars: number 23 | topics?: string[] 24 | updatedAt: string 25 | url: string 26 | } 27 | 28 | export type Request = { 29 | body: SubmitRequest 30 | comments: number 31 | createdAt: Date 32 | title: string 33 | id: number 34 | updatedAt: Date 35 | url: string 36 | } 37 | 38 | export const fetcherRequestList = async (query: string = '') => { 39 | const params = [ 40 | 'repo:sospedra/rfm', 41 | 'state:open', 42 | 'label:search', 43 | query, 44 | 'in:title,body', 45 | ] 46 | const path = `${GITHUB_ROOT}/search/issues?q=${params.join('+')}&per_page=100` 47 | const response = await fetch(path) 48 | const payload: { 49 | items: { [key: string]: any }[] 50 | total_count: number 51 | } = await response.json() 52 | const items = payload.items.map((item) => { 53 | try { 54 | return { 55 | body: JSON.parse(item.body), 56 | id: item.id, 57 | comments: item.comments, 58 | createdAt: new Date(item.created_at), 59 | title: item.title, 60 | updatedAt: new Date(item.updated_at), 61 | url: item.html_url, 62 | } 63 | } catch (ex) { 64 | return null 65 | } 66 | }) 67 | const requestList = items.filter((_) => _ !== null) as Request[] 68 | 69 | return { 70 | requestList, 71 | total: payload.total_count, 72 | } 73 | } 74 | 75 | const safe = ( 76 | collection: T | undefined, 77 | key: keyof T, 78 | name?: string, 79 | ) => { 80 | if (!collection) return {} 81 | let property = collection[key] as string | string[] | number 82 | return !!property ? { [name || key]: property } : {} 83 | } 84 | 85 | export const fetcherSubmitRequest = async (repoUrl: string) => { 86 | const [_, pathname] = repoUrl.split('github.com/') 87 | const [owner, name] = pathname.split('/') 88 | const response: { [key: string]: any } = await fetch( 89 | `${GITHUB_ROOT}/repos/${owner}/${name}`, 90 | { 91 | headers: { 92 | Accept: 'application/vnd.github.mercy-preview+json', 93 | }, 94 | }, 95 | ) 96 | const payload = await response.json() 97 | const language = langmap[payload.language] 98 | const repo: SubmitRequest = { 99 | description: payload.description, 100 | fullName: payload.full_name, 101 | language: payload.language, 102 | name, 103 | openIssues: payload.open_issues_count, 104 | owner, 105 | stars: payload.stargazers_count, 106 | updatedAt: payload.updated_at, 107 | url: payload.html_url, 108 | requestIssueFullName: 'NONE', 109 | requestIssueNumber: -1, 110 | ...safe(payload.license, 'spdx_id', 'license'), 111 | ...safe(payload, 'topics'), 112 | ...safe(language, 'filenames'), 113 | ...safe(language, 'aceMode'), 114 | ...safe(language, 'aliases'), 115 | ...safe(language, 'color'), 116 | ...safe(language, 'extensions'), 117 | ...safe(language, 'group'), 118 | ...safe(language, 'interpreters'), 119 | } 120 | 121 | return repo 122 | } 123 | 124 | export const fetcherFindSupportIssues = async (fullName: string) => { 125 | const params = [ 126 | `repo:${fullName}`, 127 | 'state:open', 128 | 'type:issue', 129 | 'support OR maintain', 130 | 'in:title,body', 131 | ] 132 | const path = `${GITHUB_ROOT}/search/issues?q=${params.join('+')}&per_page=10` 133 | const response = await fetch(path) 134 | const payload: { 135 | items: { [key: string]: any }[] 136 | total_count: number 137 | } = await response.json() 138 | const items = payload.items.map< 139 | Partial & { 140 | user: string 141 | number: number 142 | body: string 143 | } 144 | >((item) => ({ 145 | id: item.id, 146 | body: item.body, 147 | comments: item.comments, 148 | createdAt: new Date(item.created_at), 149 | title: item.title, 150 | url: item.html_url, 151 | user: item.user.login, 152 | number: item.number, 153 | })) 154 | 155 | return { 156 | requestList: items, 157 | total: payload.total_count, 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/home/List.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Code as PlaceholderCode } from 'react-content-loader' 3 | import humanNumber from 'human-number' 4 | import { Request } from '../rfm/services/api/github' 5 | import Star from '../rfm/components/Star' 6 | import Issue from '../rfm/components/Issue' 7 | import Circle from '../rfm/components/Circle' 8 | import { track } from '../rfm/services/analytics' 9 | 10 | const selectMessage = (total?: number) => { 11 | switch (total) { 12 | case 0: 13 | return ( 14 |

15 | There are not requests that matches your criteria{' '} 16 | 17 | 🤷🏻‍♂️ 18 | {' '} 19 | Try another search. 20 |

21 | ) 22 | case 1: 23 | return ( 24 |

25 | There is 1 request that needs your help{' '} 26 | 27 | 💪 28 | 29 |

30 | ) 31 | default: 32 | return ( 33 |

34 | There are {total} requests that need your help{' '} 35 | 36 | 🦸🏽‍♀️ 37 | 38 |

39 | ) 40 | } 41 | } 42 | 43 | const List: React.FC<{ 44 | requestList?: Request[] 45 | total?: number 46 | }> = (props) => { 47 | if (!props.requestList) { 48 | return ( 49 |
50 | 51 |
52 | 53 |
54 | ) 55 | } 56 | 57 | return ( 58 |
59 | {selectMessage(props.total)} 60 | 111 |
112 |

113 | 114 | ProTip 115 | 116 | ™️ 117 | {' '} 118 | 119 | Results are limited to a{' '} 120 | maximum of 100 items per search. 121 |

122 |

123 | To get the best results{' '} 124 | refine your query or inspect the{' '} 125 | 129 | repo 130 | 131 |

132 |
133 |
134 | ) 135 | } 136 | 137 | export default List 138 | -------------------------------------------------------------------------------- /src/home/Newsletter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { track } from '../rfm/services/analytics' 3 | import './newsletter.css' 4 | 5 | const FORM_ID = '1346503' 6 | 7 | const Newsletter: React.FC<{}> = () => { 8 | return ( 9 |
track('newsletter')} 12 | action={`https://app.convertkit.com/forms/${FORM_ID}/subscriptions`} 13 | className='mx-auto bg-white rounded shadow-lg formkit-form' 14 | method='post' 15 | min-width='400 500 600 700 800' 16 | > 17 |
18 |
19 |

23 | Join the newsletter 24 |

25 |
26 |

27 | Subscribe to get a monthly single email with the latest 28 | repositories added 29 |

30 |
31 |
32 | 39 | 40 | 44 | 49 | 53 | 57 | 61 | 62 | 63 |
64 |
65 |
66 |
    71 | 72 |
    73 |
    74 | 82 |
    83 |
    84 | 92 |
    93 | 100 |
    101 |
    102 |

    103 | I won’t send you spam never. 104 |

    105 |

    106 | Unsubscribe at any time. 107 |

    108 |
    109 |
110 |
111 |
112 | ) 113 | } 114 | 115 | export default Newsletter 116 | -------------------------------------------------------------------------------- /src/rfm/services/sw/index.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/, 20 | ), 21 | ) 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void 26 | } 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href) 32 | if (publicUrl.origin !== window.location.origin) { 33 | // Our service worker won't work if PUBLIC_URL is on a different origin 34 | // from what our page is served on. This might happen if a CDN is used to 35 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 36 | return 37 | } 38 | 39 | window.addEventListener('load', () => { 40 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js` 41 | 42 | if (isLocalhost) { 43 | // This is running on localhost. Let's check if a service worker still exists or not. 44 | checkValidServiceWorker(swUrl, config) 45 | 46 | // Add some additional logging to localhost, pointing developers to the 47 | // service worker/PWA documentation. 48 | navigator.serviceWorker.ready.then(() => { 49 | console.log( 50 | 'This web app is being served cache-first by a service ' + 51 | 'worker. To learn more, visit https://bit.ly/CRA-PWA', 52 | ) 53 | }) 54 | } else { 55 | // Is not localhost. Just register service worker 56 | registerValidSW(swUrl, config) 57 | } 58 | }) 59 | } 60 | } 61 | 62 | function registerValidSW(swUrl: string, config?: Config) { 63 | navigator.serviceWorker 64 | .register(swUrl) 65 | .then((registration) => { 66 | registration.onupdatefound = () => { 67 | const installingWorker = registration.installing 68 | if (installingWorker == null) { 69 | return 70 | } 71 | installingWorker.onstatechange = () => { 72 | if (installingWorker.state === 'installed') { 73 | if (navigator.serviceWorker.controller) { 74 | // At this point, the updated precached content has been fetched, 75 | // but the previous service worker will still serve the older 76 | // content until all client tabs are closed. 77 | console.log( 78 | 'New content is available and will be used when all ' + 79 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.', 80 | ) 81 | 82 | // Execute callback 83 | if (config && config.onUpdate) { 84 | config.onUpdate(registration) 85 | } 86 | } else { 87 | // At this point, everything has been precached. 88 | // It's the perfect time to display a 89 | // "Content is cached for offline use." message. 90 | console.log('Content is cached for offline use.') 91 | 92 | // Execute callback 93 | if (config && config.onSuccess) { 94 | config.onSuccess(registration) 95 | } 96 | } 97 | } 98 | } 99 | } 100 | }) 101 | .catch((error) => { 102 | console.error('Error during service worker registration:', error) 103 | }) 104 | } 105 | 106 | function checkValidServiceWorker(swUrl: string, config?: Config) { 107 | // Check if the service worker can be found. If it can't reload the page. 108 | fetch(swUrl, { 109 | headers: { 'Service-Worker': 'script' }, 110 | }) 111 | .then((response) => { 112 | // Ensure service worker exists, and that we really are getting a JS file. 113 | const contentType = response.headers.get('content-type') 114 | if ( 115 | response.status === 404 || 116 | (contentType != null && contentType.indexOf('javascript') === -1) 117 | ) { 118 | // No service worker found. Probably a different app. Reload the page. 119 | navigator.serviceWorker.ready.then((registration) => { 120 | registration.unregister().then(() => { 121 | window.location.reload() 122 | }) 123 | }) 124 | } else { 125 | // Service worker found. Proceed as normal. 126 | registerValidSW(swUrl, config) 127 | } 128 | }) 129 | .catch(() => { 130 | console.log( 131 | 'No internet connection found. App is running in offline mode.', 132 | ) 133 | }) 134 | } 135 | 136 | export function unregister() { 137 | if ('serviceWorker' in navigator) { 138 | navigator.serviceWorker.ready 139 | .then((registration) => { 140 | registration.unregister() 141 | }) 142 | .catch((error) => { 143 | console.error(error.message) 144 | }) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/submit/Issue.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { createPortal } from 'react-dom' 3 | import { List as PlaceholderList } from 'react-content-loader' 4 | import Markdown from 'markdown-to-jsx' 5 | import useSWR from 'swr' 6 | import { isValidGithubIssueUrl, NONE_ISSUE } from '../rfm/services/github' 7 | import { 8 | SubmitRequest, 9 | fetcherFindSupportIssues, 10 | } from '../rfm/services/api/github' 11 | import Comments from '../rfm/components/Comments' 12 | import Error from '../rfm/components/Error' 13 | import Button from './Button' 14 | import './issue.css' 15 | import { track } from '../rfm/services/analytics' 16 | 17 | const Issue: React.FC<{ 18 | onNext: () => void 19 | data?: SubmitRequest 20 | requestIssue: string 21 | setRequestIssue: (issue: string) => void 22 | }> = (props) => { 23 | const [didSubmit, setDidSubmit] = useState(false) 24 | const isValidUrl = isValidGithubIssueUrl(props.requestIssue) 25 | const { data, error } = useSWR( 26 | [props.data?.fullName], 27 | fetcherFindSupportIssues, 28 | ) 29 | 30 | return ( 31 |
32 |

Enter the issue link

33 |

34 | To ensure the best communication we need to know in which Github issue 35 | the owners of {props.data?.fullName} requested support to 36 | maintain the project 37 |

38 |
{ 42 | e.preventDefault() 43 | setDidSubmit(true) 44 | track('submit', { step: 'issue' }) 45 | props.onNext() 46 | }} 47 | > 48 |
49 | 50 | props.setRequestIssue(e.currentTarget.value)} 54 | placeholder={`github.com/${props.data?.fullName}/:number`} 55 | className='w-full px-4 py-2 my-4 border rounded shadow-lg' 56 | required 57 | /> 58 | 59 |
60 |

61 | Suggestions 62 |

63 |

Maybe it's one of these

64 | {data?.requestList.map( 65 | ({ id, url, title, user, createdAt, comments, number, body }) => ( 66 | 102 | ), 103 | )} 104 | {!data ? ( 105 | 106 | ) : ( 107 | 127 | )} 128 |
129 |
130 | 131 | {createPortal( 132 |
137 | 140 |
, 141 | document.getElementsByTagName('main')[0], 142 | )} 143 |
144 |
145 | ) 146 | } 147 | 148 | export default Issue 149 | -------------------------------------------------------------------------------- /src/home/newsletter.css: -------------------------------------------------------------------------------- 1 | .formkit-form * { 2 | box-sizing: border-box; 3 | } 4 | .formkit-form legend { 5 | border: none; 6 | font-size: inherit; 7 | margin-bottom: 10px; 8 | padding: 0; 9 | position: relative; 10 | display: table; 11 | } 12 | .formkit-form fieldset { 13 | border: 0; 14 | padding: 0.01em 0 0 0; 15 | margin: 0; 16 | min-width: 0; 17 | } 18 | .formkit-form body:not(:-moz-handler-blocked) fieldset { 19 | display: table-cell; 20 | } 21 | .formkit-form p { 22 | color: inherit; 23 | font-size: inherit; 24 | font-weight: inherit; 25 | } 26 | .formkit-form[data-format='modal'] { 27 | display: none; 28 | } 29 | .formkit-form[data-format='slide in'] { 30 | display: none; 31 | } 32 | .formkit-form .formkit-input, 33 | .formkit-form .formkit-select, 34 | .formkit-form .formkit-checkboxes { 35 | width: 100%; 36 | } 37 | .formkit-form .formkit-button, 38 | .formkit-form .formkit-submit { 39 | border: 0; 40 | border-radius: 5px; 41 | color: #ffffff; 42 | cursor: pointer; 43 | display: inline-block; 44 | text-align: center; 45 | font-size: 15px; 46 | font-weight: 500; 47 | margin-bottom: 15px; 48 | overflow: hidden; 49 | padding: 0; 50 | position: relative; 51 | vertical-align: middle; 52 | } 53 | .formkit-form .formkit-button:hover, 54 | .formkit-form .formkit-submit:hover, 55 | .formkit-form .formkit-button:focus, 56 | .formkit-form .formkit-submit:focus { 57 | outline: none; 58 | } 59 | .formkit-form .formkit-button:hover > span, 60 | .formkit-form .formkit-submit:hover > span, 61 | .formkit-form .formkit-button:focus > span, 62 | .formkit-form .formkit-submit:focus > span { 63 | background-color: hsl(340, 63%, 63%); 64 | } 65 | .formkit-form .formkit-button > span, 66 | .formkit-form .formkit-submit > span { 67 | display: block; 68 | -webkit-transition: all 300ms ease-in-out; 69 | transition: all 300ms ease-in-out; 70 | padding: 12px 24px; 71 | } 72 | .formkit-form .formkit-input { 73 | background: #ffffff; 74 | font-size: 15px; 75 | padding: 12px; 76 | border: 1px solid #e3e3e3; 77 | -webkit-flex: 1 0 auto; 78 | -ms-flex: 1 0 auto; 79 | flex: 1 0 auto; 80 | line-height: 1.4; 81 | margin: 0; 82 | -webkit-transition: border-color ease-out 300ms; 83 | transition: border-color ease-out 300ms; 84 | } 85 | .formkit-form .formkit-input:focus { 86 | outline: none; 87 | border-color: #1677be; 88 | -webkit-transition: border-color ease 300ms; 89 | transition: border-color ease 300ms; 90 | } 91 | .formkit-form .formkit-input::-webkit-input-placeholder { 92 | color: #848585; 93 | } 94 | .formkit-form .formkit-input::-moz-placeholder { 95 | color: #848585; 96 | } 97 | .formkit-form .formkit-input:-ms-input-placeholder { 98 | color: #848585; 99 | } 100 | .formkit-form .formkit-input::placeholder { 101 | color: #848585; 102 | } 103 | .formkit-form [data-group='dropdown'] { 104 | position: relative; 105 | display: inline-block; 106 | width: 100%; 107 | } 108 | .formkit-form [data-group='dropdown']::before { 109 | content: ''; 110 | top: calc(50% - 2.5px); 111 | right: 10px; 112 | position: absolute; 113 | pointer-events: none; 114 | border-color: #4f4f4f transparent transparent transparent; 115 | border-style: solid; 116 | border-width: 6px 6px 0 6px; 117 | height: 0; 118 | width: 0; 119 | z-index: 999; 120 | } 121 | .formkit-form [data-group='dropdown'] select { 122 | height: auto; 123 | width: 100%; 124 | cursor: pointer; 125 | color: #333333; 126 | line-height: 1.4; 127 | margin-bottom: 0; 128 | -webkit-appearance: none; 129 | -moz-appearance: none; 130 | appearance: none; 131 | font-size: 15px; 132 | padding: 12px 25px 12px 12px; 133 | border: 1px solid #e3e3e3; 134 | background: #ffffff; 135 | } 136 | .formkit-form [data-group='dropdown'] select:focus { 137 | outline: none; 138 | } 139 | .formkit-form [data-group='checkboxes'] { 140 | text-align: left; 141 | margin: 0; 142 | } 143 | .formkit-form [data-group='checkboxes'] [data-group='checkbox'] { 144 | margin-bottom: 10px; 145 | } 146 | .formkit-form [data-group='checkboxes'] [data-group='checkbox'] * { 147 | cursor: pointer; 148 | } 149 | .formkit-form [data-group='checkboxes'] [data-group='checkbox']:last-of-type { 150 | margin-bottom: 0; 151 | } 152 | .formkit-form 153 | [data-group='checkboxes'] 154 | [data-group='checkbox'] 155 | input[type='checkbox'] { 156 | display: none; 157 | } 158 | .formkit-form 159 | [data-group='checkboxes'] 160 | [data-group='checkbox'] 161 | input[type='checkbox'] 162 | + label::after { 163 | content: none; 164 | } 165 | .formkit-form 166 | [data-group='checkboxes'] 167 | [data-group='checkbox'] 168 | input[type='checkbox']:checked 169 | + label::after { 170 | border-color: #ffffff; 171 | content: ''; 172 | } 173 | .formkit-form 174 | [data-group='checkboxes'] 175 | [data-group='checkbox'] 176 | input[type='checkbox']:checked 177 | + label::before { 178 | background: #10bf7a; 179 | border-color: #10bf7a; 180 | } 181 | .formkit-form [data-group='checkboxes'] [data-group='checkbox'] label { 182 | position: relative; 183 | display: inline-block; 184 | padding-left: 28px; 185 | } 186 | .formkit-form [data-group='checkboxes'] [data-group='checkbox'] label::before, 187 | .formkit-form [data-group='checkboxes'] [data-group='checkbox'] label::after { 188 | position: absolute; 189 | content: ''; 190 | display: inline-block; 191 | } 192 | .formkit-form [data-group='checkboxes'] [data-group='checkbox'] label::before { 193 | height: 16px; 194 | width: 16px; 195 | border: 1px solid #e3e3e3; 196 | background: #ffffff; 197 | left: 0; 198 | top: 3px; 199 | } 200 | .formkit-form [data-group='checkboxes'] [data-group='checkbox'] label::after { 201 | height: 4px; 202 | width: 8px; 203 | border-left: 2px solid #4d4d4d; 204 | border-bottom: 2px solid #4d4d4d; 205 | -webkit-transform: rotate(-45deg); 206 | -ms-transform: rotate(-45deg); 207 | transform: rotate(-45deg); 208 | left: 4px; 209 | top: 8px; 210 | } 211 | .formkit-form .formkit-alert { 212 | background: #f9fafb; 213 | border: 1px solid #e3e3e3; 214 | border-radius: 5px; 215 | -webkit-flex: 1 0 auto; 216 | -ms-flex: 1 0 auto; 217 | flex: 1 0 auto; 218 | list-style: none; 219 | margin: 25px auto; 220 | padding: 12px; 221 | text-align: center; 222 | width: 100%; 223 | } 224 | .formkit-form .formkit-alert:empty { 225 | display: none; 226 | } 227 | .formkit-form .formkit-alert-success { 228 | background: #d3fbeb; 229 | border-color: #10bf7a; 230 | color: #0c905c; 231 | } 232 | .formkit-form .formkit-alert-error { 233 | background: #fde8e2; 234 | border-color: #f2643b; 235 | color: #ea4110; 236 | } 237 | .formkit-form .formkit-spinner { 238 | display: -webkit-box; 239 | display: -webkit-flex; 240 | display: -ms-flexbox; 241 | display: flex; 242 | height: 0; 243 | width: 0; 244 | margin: 0 auto; 245 | position: absolute; 246 | top: 0; 247 | left: 0; 248 | right: 0; 249 | overflow: hidden; 250 | text-align: center; 251 | -webkit-transition: all 300ms ease-in-out; 252 | transition: all 300ms ease-in-out; 253 | } 254 | .formkit-form .formkit-spinner > div { 255 | margin: auto; 256 | width: 12px; 257 | height: 12px; 258 | background-color: #fff; 259 | opacity: 0.3; 260 | border-radius: 100%; 261 | display: inline-block; 262 | -webkit-animation: formkit-bouncedelay-formkit-form-data-uid-4a352cb1fd- 1.4s 263 | infinite ease-in-out both; 264 | animation: formkit-bouncedelay-formkit-form-data-uid-4a352cb1fd- 1.4s infinite 265 | ease-in-out both; 266 | } 267 | .formkit-form .formkit-spinner > div:nth-child(1) { 268 | -webkit-animation-delay: -0.32s; 269 | animation-delay: -0.32s; 270 | } 271 | .formkit-form .formkit-spinner > div:nth-child(2) { 272 | -webkit-animation-delay: -0.16s; 273 | animation-delay: -0.16s; 274 | } 275 | .formkit-form .formkit-submit[data-active] .formkit-spinner { 276 | opacity: 1; 277 | height: 100%; 278 | width: 50px; 279 | } 280 | .formkit-form .formkit-submit[data-active] .formkit-spinner ~ span { 281 | opacity: 0; 282 | } 283 | @-webkit-keyframes formkit-bouncedelay-formkit-form-data-uid-4a352cb1fd- { 284 | 0%, 285 | 80%, 286 | 100% { 287 | -webkit-transform: scale(0); 288 | -ms-transform: scale(0); 289 | transform: scale(0); 290 | } 291 | 40% { 292 | -webkit-transform: scale(1); 293 | -ms-transform: scale(1); 294 | transform: scale(1); 295 | } 296 | } 297 | @keyframes formkit-bouncedelay-formkit-form-data-uid-4a352cb1fd- { 298 | 0%, 299 | 80%, 300 | 100% { 301 | -webkit-transform: scale(0); 302 | -ms-transform: scale(0); 303 | transform: scale(0); 304 | } 305 | 40% { 306 | -webkit-transform: scale(1); 307 | -ms-transform: scale(1); 308 | transform: scale(1); 309 | } 310 | } 311 | .formkit-form { 312 | max-width: 700px; 313 | overflow: hidden; 314 | } 315 | .formkit-form [data-style='full'] { 316 | width: 100%; 317 | display: block; 318 | } 319 | .formkit-form .formkit-header { 320 | margin-top: 0; 321 | margin-bottom: 20px; 322 | font-family: inherit; 323 | } 324 | .formkit-form .formkit-subheader { 325 | margin: 15px 0; 326 | } 327 | .formkit-form .formkit-column { 328 | background-size: cover; 329 | background-repeat: no-repeat; 330 | background-position: center; 331 | } 332 | .formkit-form .formkit-column:nth-child(2) { 333 | border-top: 1px solid #e9ecef; 334 | } 335 | .formkit-form .formkit-field { 336 | margin: 0 0 15px 0; 337 | } 338 | .formkit-form .formkit-input, 339 | .formkit-form .formkit-submit { 340 | width: 100%; 341 | } 342 | .formkit-form .formkit-guarantee { 343 | font-size: 13px; 344 | margin: 0 0 15px 0; 345 | } 346 | .formkit-form .formkit-guarantee > p { 347 | margin: 0; 348 | } 349 | .formkit-form .formkit-powered-by { 350 | color: #7d7d7d; 351 | display: block; 352 | font-size: 12px; 353 | margin-bottom: 0; 354 | text-align: center; 355 | } 356 | .formkit-form .formkit-powered-by[data-active='false'] { 357 | opacity: 0.5; 358 | } 359 | .formkit-form[min-width~='600'] [data-style='full'], 360 | .formkit-form[min-width~='700'] [data-style='full'], 361 | .formkit-form[min-width~='800'] [data-style='full'] { 362 | display: grid; 363 | grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 364 | } 365 | .formkit-form[min-width~='600'] .formkit-submit, 366 | .formkit-form[min-width~='700'] .formkit-submit, 367 | .formkit-form[min-width~='800'] .formkit-submit { 368 | width: auto; 369 | } 370 | .formkit-form[min-width~='600'] .formkit-column:nth-child(2), 371 | .formkit-form[min-width~='700'] .formkit-column:nth-child(2), 372 | .formkit-form[min-width~='800'] .formkit-column:nth-child(2) { 373 | border-top: none; 374 | } 375 | -------------------------------------------------------------------------------- /src/about/markdown.css: -------------------------------------------------------------------------------- 1 | .markdown-body ol { 2 | list-style: decimal; 3 | } 4 | 5 | .markdown-body ul { 6 | list-style: disc; 7 | } 8 | 9 | .markdown-body h3 { 10 | font-family: monospace; 11 | } 12 | 13 | .markdown-body .octicon { 14 | display: inline-block; 15 | fill: currentColor; 16 | vertical-align: text-bottom; 17 | } 18 | 19 | .markdown-body .anchor { 20 | float: left; 21 | line-height: 1; 22 | margin-left: -20px; 23 | padding-right: 4px; 24 | } 25 | 26 | .markdown-body .anchor:focus { 27 | outline: none; 28 | } 29 | 30 | .markdown-body h1 .octicon-link, 31 | .markdown-body h2 .octicon-link, 32 | .markdown-body h3 .octicon-link, 33 | .markdown-body h4 .octicon-link, 34 | .markdown-body h5 .octicon-link, 35 | .markdown-body h6 .octicon-link { 36 | color: #1b1f23; 37 | vertical-align: middle; 38 | visibility: hidden; 39 | } 40 | 41 | .markdown-body h1:hover .anchor, 42 | .markdown-body h2:hover .anchor, 43 | .markdown-body h3:hover .anchor, 44 | .markdown-body h4:hover .anchor, 45 | .markdown-body h5:hover .anchor, 46 | .markdown-body h6:hover .anchor { 47 | text-decoration: none; 48 | } 49 | 50 | .markdown-body h1:hover .anchor .octicon-link, 51 | .markdown-body h2:hover .anchor .octicon-link, 52 | .markdown-body h3:hover .anchor .octicon-link, 53 | .markdown-body h4:hover .anchor .octicon-link, 54 | .markdown-body h5:hover .anchor .octicon-link, 55 | .markdown-body h6:hover .anchor .octicon-link { 56 | visibility: visible; 57 | } 58 | 59 | .markdown-body h1:hover .anchor .octicon-link:before, 60 | .markdown-body h2:hover .anchor .octicon-link:before, 61 | .markdown-body h3:hover .anchor .octicon-link:before, 62 | .markdown-body h4:hover .anchor .octicon-link:before, 63 | .markdown-body h5:hover .anchor .octicon-link:before, 64 | .markdown-body h6:hover .anchor .octicon-link:before { 65 | width: 16px; 66 | height: 16px; 67 | content: ' '; 68 | display: inline-block; 69 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' width='16' height='16' aria-hidden='true'%3E%3Cpath fill-rule='evenodd' d='M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z'%3E%3C/path%3E%3C/svg%3E"); 70 | } 71 | .markdown-body { 72 | -ms-text-size-adjust: 100%; 73 | -webkit-text-size-adjust: 100%; 74 | line-height: 1.5; 75 | color: #24292e; 76 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, 77 | sans-serif, Apple Color Emoji, Segoe UI Emoji; 78 | font-size: 16px; 79 | line-height: 1.5; 80 | word-wrap: break-word; 81 | } 82 | 83 | .markdown-body details { 84 | display: block; 85 | } 86 | 87 | .markdown-body summary { 88 | display: list-item; 89 | } 90 | 91 | .markdown-body a { 92 | background-color: initial; 93 | } 94 | 95 | .markdown-body a:active, 96 | .markdown-body a:hover { 97 | outline-width: 0; 98 | } 99 | 100 | .markdown-body strong { 101 | font-weight: inherit; 102 | font-weight: bolder; 103 | } 104 | 105 | .markdown-body h1 { 106 | font-size: 2em; 107 | margin: 0.67em 0; 108 | } 109 | 110 | .markdown-body img { 111 | border-style: none; 112 | } 113 | 114 | .markdown-body code, 115 | .markdown-body kbd, 116 | .markdown-body pre { 117 | font-family: monospace, monospace; 118 | font-size: 1em; 119 | } 120 | 121 | .markdown-body hr { 122 | box-sizing: initial; 123 | height: 0; 124 | overflow: visible; 125 | } 126 | 127 | .markdown-body input { 128 | font: inherit; 129 | margin: 0; 130 | } 131 | 132 | .markdown-body input { 133 | overflow: visible; 134 | } 135 | 136 | .markdown-body [type='checkbox'] { 137 | box-sizing: border-box; 138 | padding: 0; 139 | } 140 | 141 | .markdown-body * { 142 | box-sizing: border-box; 143 | } 144 | 145 | .markdown-body input { 146 | font-family: inherit; 147 | font-size: inherit; 148 | line-height: inherit; 149 | } 150 | 151 | .markdown-body a { 152 | color: #0366d6; 153 | text-decoration: none; 154 | } 155 | 156 | .markdown-body a:hover { 157 | text-decoration: underline; 158 | } 159 | 160 | .markdown-body strong { 161 | font-weight: 600; 162 | } 163 | 164 | .markdown-body hr { 165 | height: 0; 166 | margin: 15px 0; 167 | overflow: hidden; 168 | background: transparent; 169 | border: 0; 170 | border-bottom: 1px solid #dfe2e5; 171 | } 172 | 173 | .markdown-body hr:after, 174 | .markdown-body hr:before { 175 | display: table; 176 | content: ''; 177 | } 178 | 179 | .markdown-body hr:after { 180 | clear: both; 181 | } 182 | 183 | .markdown-body table { 184 | border-spacing: 0; 185 | border-collapse: collapse; 186 | } 187 | 188 | .markdown-body td, 189 | .markdown-body th { 190 | padding: 0; 191 | } 192 | 193 | .markdown-body details summary { 194 | cursor: pointer; 195 | } 196 | 197 | .markdown-body kbd { 198 | display: inline-block; 199 | padding: 3px 5px; 200 | font: 11px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; 201 | line-height: 10px; 202 | color: #444d56; 203 | vertical-align: middle; 204 | background-color: #fafbfc; 205 | border: 1px solid #d1d5da; 206 | border-radius: 3px; 207 | box-shadow: inset 0 -1px 0 #d1d5da; 208 | } 209 | 210 | .markdown-body h1, 211 | .markdown-body h2, 212 | .markdown-body h3, 213 | .markdown-body h4, 214 | .markdown-body h5, 215 | .markdown-body h6 { 216 | margin-top: 0; 217 | margin-bottom: 0; 218 | } 219 | 220 | .markdown-body h1 { 221 | font-size: 32px; 222 | } 223 | 224 | .markdown-body h1, 225 | .markdown-body h2 { 226 | font-weight: 600; 227 | } 228 | 229 | .markdown-body h2 { 230 | font-size: 24px; 231 | } 232 | 233 | .markdown-body h3 { 234 | font-size: 20px; 235 | } 236 | 237 | .markdown-body h3, 238 | .markdown-body h4 { 239 | font-weight: 900; 240 | } 241 | 242 | .markdown-body h4 { 243 | font-size: 16px; 244 | } 245 | 246 | .markdown-body h5 { 247 | font-size: 14px; 248 | } 249 | 250 | .markdown-body h5, 251 | .markdown-body h6 { 252 | font-weight: 600; 253 | } 254 | 255 | .markdown-body h6 { 256 | font-size: 12px; 257 | } 258 | 259 | .markdown-body p { 260 | margin-top: 0; 261 | margin-bottom: 10px; 262 | } 263 | 264 | .markdown-body blockquote { 265 | margin: 0; 266 | } 267 | 268 | .markdown-body ol, 269 | .markdown-body ul { 270 | padding-left: 0; 271 | margin-top: 0; 272 | margin-bottom: 0; 273 | } 274 | 275 | .markdown-body ol ol, 276 | .markdown-body ul ol { 277 | list-style-type: lower-roman; 278 | } 279 | 280 | .markdown-body ol ol ol, 281 | .markdown-body ol ul ol, 282 | .markdown-body ul ol ol, 283 | .markdown-body ul ul ol { 284 | list-style-type: lower-alpha; 285 | } 286 | 287 | .markdown-body dd { 288 | margin-left: 0; 289 | } 290 | 291 | .markdown-body code, 292 | .markdown-body pre { 293 | font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; 294 | font-size: 12px; 295 | } 296 | 297 | .markdown-body pre { 298 | margin-top: 0; 299 | margin-bottom: 0; 300 | } 301 | 302 | .markdown-body input::-webkit-inner-spin-button, 303 | .markdown-body input::-webkit-outer-spin-button { 304 | margin: 0; 305 | -webkit-appearance: none; 306 | appearance: none; 307 | } 308 | 309 | .markdown-body :checked + .radio-label { 310 | position: relative; 311 | z-index: 1; 312 | border-color: #0366d6; 313 | } 314 | 315 | .markdown-body .border { 316 | border: 1px solid #e1e4e8 !important; 317 | } 318 | 319 | .markdown-body .border-0 { 320 | border: 0 !important; 321 | } 322 | 323 | .markdown-body .border-bottom { 324 | border-bottom: 1px solid #e1e4e8 !important; 325 | } 326 | 327 | .markdown-body .rounded-1 { 328 | border-radius: 3px !important; 329 | } 330 | 331 | .markdown-body .bg-white { 332 | background-color: #fff !important; 333 | } 334 | 335 | .markdown-body .bg-gray-light { 336 | background-color: #fafbfc !important; 337 | } 338 | 339 | .markdown-body .text-gray-light { 340 | color: #6a737d !important; 341 | } 342 | 343 | .markdown-body .mb-0 { 344 | margin-bottom: 0 !important; 345 | } 346 | 347 | .markdown-body .my-2 { 348 | margin-top: 8px !important; 349 | margin-bottom: 8px !important; 350 | } 351 | 352 | .markdown-body .pl-0 { 353 | padding-left: 0 !important; 354 | } 355 | 356 | .markdown-body .py-0 { 357 | padding-top: 0 !important; 358 | padding-bottom: 0 !important; 359 | } 360 | 361 | .markdown-body .pl-1 { 362 | padding-left: 4px !important; 363 | } 364 | 365 | .markdown-body .pl-2 { 366 | padding-left: 8px !important; 367 | } 368 | 369 | .markdown-body .py-2 { 370 | padding-top: 8px !important; 371 | padding-bottom: 8px !important; 372 | } 373 | 374 | .markdown-body .pl-3, 375 | .markdown-body .px-3 { 376 | padding-left: 16px !important; 377 | } 378 | 379 | .markdown-body .px-3 { 380 | padding-right: 16px !important; 381 | } 382 | 383 | .markdown-body .pl-4 { 384 | padding-left: 24px !important; 385 | } 386 | 387 | .markdown-body .pl-5 { 388 | padding-left: 32px !important; 389 | } 390 | 391 | .markdown-body .pl-6 { 392 | padding-left: 40px !important; 393 | } 394 | 395 | .markdown-body .f6 { 396 | font-size: 12px !important; 397 | } 398 | 399 | .markdown-body .lh-condensed { 400 | line-height: 1.25 !important; 401 | } 402 | 403 | .markdown-body .text-bold { 404 | font-weight: 600 !important; 405 | } 406 | 407 | .markdown-body .pl-c { 408 | color: #6a737d; 409 | } 410 | 411 | .markdown-body .pl-c1, 412 | .markdown-body .pl-s .pl-v { 413 | color: #005cc5; 414 | } 415 | 416 | .markdown-body .pl-e, 417 | .markdown-body .pl-en { 418 | color: #6f42c1; 419 | } 420 | 421 | .markdown-body .pl-s .pl-s1, 422 | .markdown-body .pl-smi { 423 | color: #24292e; 424 | } 425 | 426 | .markdown-body .pl-ent { 427 | color: #22863a; 428 | } 429 | 430 | .markdown-body .pl-k { 431 | color: #d73a49; 432 | } 433 | 434 | .markdown-body .pl-pds, 435 | .markdown-body .pl-s, 436 | .markdown-body .pl-s .pl-pse .pl-s1, 437 | .markdown-body .pl-sr, 438 | .markdown-body .pl-sr .pl-cce, 439 | .markdown-body .pl-sr .pl-sra, 440 | .markdown-body .pl-sr .pl-sre { 441 | color: #032f62; 442 | } 443 | 444 | .markdown-body .pl-smw, 445 | .markdown-body .pl-v { 446 | color: #e36209; 447 | } 448 | 449 | .markdown-body .pl-bu { 450 | color: #b31d28; 451 | } 452 | 453 | .markdown-body .pl-ii { 454 | color: #fafbfc; 455 | background-color: #b31d28; 456 | } 457 | 458 | .markdown-body .pl-c2 { 459 | color: #fafbfc; 460 | background-color: #d73a49; 461 | } 462 | 463 | .markdown-body .pl-c2:before { 464 | content: '^M'; 465 | } 466 | 467 | .markdown-body .pl-sr .pl-cce { 468 | font-weight: 700; 469 | color: #22863a; 470 | } 471 | 472 | .markdown-body .pl-ml { 473 | color: #735c0f; 474 | } 475 | 476 | .markdown-body .pl-mh, 477 | .markdown-body .pl-mh .pl-en, 478 | .markdown-body .pl-ms { 479 | font-weight: 700; 480 | color: #005cc5; 481 | } 482 | 483 | .markdown-body .pl-mi { 484 | font-style: italic; 485 | color: #24292e; 486 | } 487 | 488 | .markdown-body .pl-mb { 489 | font-weight: 700; 490 | color: #24292e; 491 | } 492 | 493 | .markdown-body .pl-md { 494 | color: #b31d28; 495 | background-color: #ffeef0; 496 | } 497 | 498 | .markdown-body .pl-mi1 { 499 | color: #22863a; 500 | background-color: #f0fff4; 501 | } 502 | 503 | .markdown-body .pl-mc { 504 | color: #e36209; 505 | background-color: #ffebda; 506 | } 507 | 508 | .markdown-body .pl-mi2 { 509 | color: #f6f8fa; 510 | background-color: #005cc5; 511 | } 512 | 513 | .markdown-body .pl-mdr { 514 | font-weight: 700; 515 | color: #6f42c1; 516 | } 517 | 518 | .markdown-body .pl-ba { 519 | color: #586069; 520 | } 521 | 522 | .markdown-body .pl-sg { 523 | color: #959da5; 524 | } 525 | 526 | .markdown-body .pl-corl { 527 | text-decoration: underline; 528 | color: #032f62; 529 | } 530 | 531 | .markdown-body .mb-0 { 532 | margin-bottom: 0 !important; 533 | } 534 | 535 | .markdown-body .my-2 { 536 | margin-bottom: 8px !important; 537 | } 538 | 539 | .markdown-body .my-2 { 540 | margin-top: 8px !important; 541 | } 542 | 543 | .markdown-body .pl-0 { 544 | padding-left: 0 !important; 545 | } 546 | 547 | .markdown-body .py-0 { 548 | padding-top: 0 !important; 549 | padding-bottom: 0 !important; 550 | } 551 | 552 | .markdown-body .pl-1 { 553 | padding-left: 4px !important; 554 | } 555 | 556 | .markdown-body .pl-2 { 557 | padding-left: 8px !important; 558 | } 559 | 560 | .markdown-body .py-2 { 561 | padding-top: 8px !important; 562 | padding-bottom: 8px !important; 563 | } 564 | 565 | .markdown-body .pl-3 { 566 | padding-left: 16px !important; 567 | } 568 | 569 | .markdown-body .pl-4 { 570 | padding-left: 24px !important; 571 | } 572 | 573 | .markdown-body .pl-5 { 574 | padding-left: 32px !important; 575 | } 576 | 577 | .markdown-body .pl-6 { 578 | padding-left: 40px !important; 579 | } 580 | 581 | .markdown-body .pl-7 { 582 | padding-left: 48px !important; 583 | } 584 | 585 | .markdown-body .pl-8 { 586 | padding-left: 64px !important; 587 | } 588 | 589 | .markdown-body .pl-9 { 590 | padding-left: 80px !important; 591 | } 592 | 593 | .markdown-body .pl-10 { 594 | padding-left: 96px !important; 595 | } 596 | 597 | .markdown-body .pl-11 { 598 | padding-left: 112px !important; 599 | } 600 | 601 | .markdown-body .pl-12 { 602 | padding-left: 128px !important; 603 | } 604 | 605 | .markdown-body hr { 606 | border-bottom-color: #eee; 607 | } 608 | 609 | .markdown-body kbd { 610 | display: inline-block; 611 | padding: 3px 5px; 612 | font: 11px SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; 613 | line-height: 10px; 614 | color: #444d56; 615 | vertical-align: middle; 616 | background-color: #fafbfc; 617 | border: 1px solid #d1d5da; 618 | border-radius: 3px; 619 | box-shadow: inset 0 -1px 0 #d1d5da; 620 | } 621 | 622 | .markdown-body:after, 623 | .markdown-body:before { 624 | display: table; 625 | content: ''; 626 | } 627 | 628 | .markdown-body:after { 629 | clear: both; 630 | } 631 | 632 | .markdown-body > :first-child { 633 | margin-top: 0 !important; 634 | } 635 | 636 | .markdown-body > :last-child { 637 | margin-bottom: 0 !important; 638 | } 639 | 640 | .markdown-body a:not([href]) { 641 | color: inherit; 642 | text-decoration: none; 643 | } 644 | 645 | .markdown-body blockquote, 646 | .markdown-body details, 647 | .markdown-body dl, 648 | .markdown-body ol, 649 | .markdown-body p, 650 | .markdown-body pre, 651 | .markdown-body table, 652 | .markdown-body ul { 653 | margin-top: 0; 654 | margin-bottom: 16px; 655 | } 656 | 657 | .markdown-body hr { 658 | height: 0.25em; 659 | padding: 0; 660 | margin: 24px 0; 661 | background-color: #e1e4e8; 662 | border: 0; 663 | } 664 | 665 | .markdown-body blockquote { 666 | padding: 0 1em; 667 | color: #6a737d; 668 | border-left: 0.25em solid #dfe2e5; 669 | } 670 | 671 | .markdown-body blockquote > :first-child { 672 | margin-top: 0; 673 | } 674 | 675 | .markdown-body blockquote > :last-child { 676 | margin-bottom: 0; 677 | } 678 | 679 | .markdown-body h1, 680 | .markdown-body h2, 681 | .markdown-body h3, 682 | .markdown-body h4, 683 | .markdown-body h5, 684 | .markdown-body h6 { 685 | margin-top: 24px; 686 | margin-bottom: 16px; 687 | font-weight: 900; 688 | line-height: 1.25; 689 | } 690 | 691 | .markdown-body h1 { 692 | font-size: 2em; 693 | } 694 | 695 | .markdown-body h1, 696 | .markdown-body h2 { 697 | padding-bottom: 0.3em; 698 | border-bottom: 1px solid #eaecef; 699 | } 700 | 701 | .markdown-body h2 { 702 | font-size: 1.5em; 703 | } 704 | 705 | .markdown-body h3 { 706 | font-size: 1.25em; 707 | } 708 | 709 | .markdown-body h4 { 710 | font-size: 1em; 711 | } 712 | 713 | .markdown-body h5 { 714 | font-size: 0.875em; 715 | } 716 | 717 | .markdown-body h6 { 718 | font-size: 0.85em; 719 | color: #6a737d; 720 | } 721 | 722 | .markdown-body ol, 723 | .markdown-body ul { 724 | padding-left: 2em; 725 | } 726 | 727 | .markdown-body ol ol, 728 | .markdown-body ol ul, 729 | .markdown-body ul ol, 730 | .markdown-body ul ul { 731 | margin-top: 0; 732 | margin-bottom: 0; 733 | } 734 | 735 | .markdown-body li { 736 | word-wrap: break-all; 737 | } 738 | 739 | .markdown-body li > p { 740 | margin-top: 16px; 741 | } 742 | 743 | .markdown-body li + li { 744 | margin-top: 0.25em; 745 | } 746 | 747 | .markdown-body dl { 748 | padding: 0; 749 | } 750 | 751 | .markdown-body dl dt { 752 | padding: 0; 753 | margin-top: 16px; 754 | font-size: 1em; 755 | font-style: italic; 756 | font-weight: 600; 757 | } 758 | 759 | .markdown-body dl dd { 760 | padding: 0 16px; 761 | margin-bottom: 16px; 762 | } 763 | 764 | .markdown-body table { 765 | display: block; 766 | width: 100%; 767 | overflow: auto; 768 | } 769 | 770 | .markdown-body table th { 771 | font-weight: 600; 772 | } 773 | 774 | .markdown-body table td, 775 | .markdown-body table th { 776 | padding: 6px 13px; 777 | border: 1px solid #dfe2e5; 778 | } 779 | 780 | .markdown-body table tr { 781 | background-color: #fff; 782 | border-top: 1px solid #c6cbd1; 783 | } 784 | 785 | .markdown-body table tr:nth-child(2n) { 786 | background-color: #f6f8fa; 787 | } 788 | 789 | .markdown-body img { 790 | max-width: 100%; 791 | box-sizing: initial; 792 | background-color: #fff; 793 | } 794 | 795 | .markdown-body img[align='right'] { 796 | padding-left: 20px; 797 | } 798 | 799 | .markdown-body img[align='left'] { 800 | padding-right: 20px; 801 | } 802 | 803 | .markdown-body code { 804 | padding: 0.2em 0.4em; 805 | margin: 0; 806 | font-size: 85%; 807 | background-color: rgba(27, 31, 35, 0.05); 808 | border-radius: 3px; 809 | } 810 | 811 | .markdown-body pre { 812 | word-wrap: normal; 813 | } 814 | 815 | .markdown-body pre > code { 816 | padding: 0; 817 | margin: 0; 818 | font-size: 100%; 819 | word-break: normal; 820 | white-space: pre; 821 | background: transparent; 822 | border: 0; 823 | } 824 | 825 | .markdown-body .highlight { 826 | margin-bottom: 16px; 827 | } 828 | 829 | .markdown-body .highlight pre { 830 | margin-bottom: 0; 831 | word-break: normal; 832 | } 833 | 834 | .markdown-body .highlight pre, 835 | .markdown-body pre { 836 | padding: 16px; 837 | overflow: auto; 838 | font-size: 85%; 839 | line-height: 1.45; 840 | background-color: #f6f8fa; 841 | border-radius: 3px; 842 | } 843 | 844 | .markdown-body pre code { 845 | display: inline; 846 | max-width: auto; 847 | padding: 0; 848 | margin: 0; 849 | overflow: visible; 850 | line-height: inherit; 851 | word-wrap: normal; 852 | background-color: initial; 853 | border: 0; 854 | } 855 | 856 | .markdown-body .commit-tease-sha { 857 | display: inline-block; 858 | font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; 859 | font-size: 90%; 860 | color: #444d56; 861 | } 862 | 863 | .markdown-body .full-commit .btn-outline:not(:disabled):hover { 864 | color: #005cc5; 865 | border-color: #005cc5; 866 | } 867 | 868 | .markdown-body .blob-wrapper { 869 | overflow-x: auto; 870 | overflow-y: hidden; 871 | } 872 | 873 | .markdown-body .blob-wrapper-embedded { 874 | max-height: 240px; 875 | overflow-y: auto; 876 | } 877 | 878 | .markdown-body .blob-num { 879 | width: 1%; 880 | min-width: 50px; 881 | padding-right: 10px; 882 | padding-left: 10px; 883 | font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; 884 | font-size: 12px; 885 | line-height: 20px; 886 | color: rgba(27, 31, 35, 0.3); 887 | text-align: right; 888 | white-space: nowrap; 889 | vertical-align: top; 890 | cursor: pointer; 891 | -webkit-user-select: none; 892 | -moz-user-select: none; 893 | -ms-user-select: none; 894 | user-select: none; 895 | } 896 | 897 | .markdown-body .blob-num:hover { 898 | color: rgba(27, 31, 35, 0.6); 899 | } 900 | 901 | .markdown-body .blob-num:before { 902 | content: attr(data-line-number); 903 | } 904 | 905 | .markdown-body .blob-code { 906 | position: relative; 907 | padding-right: 10px; 908 | padding-left: 10px; 909 | line-height: 20px; 910 | vertical-align: top; 911 | } 912 | 913 | .markdown-body .blob-code-inner { 914 | overflow: visible; 915 | font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; 916 | font-size: 12px; 917 | color: #24292e; 918 | word-wrap: normal; 919 | white-space: pre; 920 | } 921 | 922 | .markdown-body .pl-token.active, 923 | .markdown-body .pl-token:hover { 924 | cursor: pointer; 925 | background: #ffea7f; 926 | } 927 | 928 | .markdown-body .tab-size[data-tab-size='1'] { 929 | -moz-tab-size: 1; 930 | tab-size: 1; 931 | } 932 | 933 | .markdown-body .tab-size[data-tab-size='2'] { 934 | -moz-tab-size: 2; 935 | tab-size: 2; 936 | } 937 | 938 | .markdown-body .tab-size[data-tab-size='3'] { 939 | -moz-tab-size: 3; 940 | tab-size: 3; 941 | } 942 | 943 | .markdown-body .tab-size[data-tab-size='4'] { 944 | -moz-tab-size: 4; 945 | tab-size: 4; 946 | } 947 | 948 | .markdown-body .tab-size[data-tab-size='5'] { 949 | -moz-tab-size: 5; 950 | tab-size: 5; 951 | } 952 | 953 | .markdown-body .tab-size[data-tab-size='6'] { 954 | -moz-tab-size: 6; 955 | tab-size: 6; 956 | } 957 | 958 | .markdown-body .tab-size[data-tab-size='7'] { 959 | -moz-tab-size: 7; 960 | tab-size: 7; 961 | } 962 | 963 | .markdown-body .tab-size[data-tab-size='8'] { 964 | -moz-tab-size: 8; 965 | tab-size: 8; 966 | } 967 | 968 | .markdown-body .tab-size[data-tab-size='9'] { 969 | -moz-tab-size: 9; 970 | tab-size: 9; 971 | } 972 | 973 | .markdown-body .tab-size[data-tab-size='10'] { 974 | -moz-tab-size: 10; 975 | tab-size: 10; 976 | } 977 | 978 | .markdown-body .tab-size[data-tab-size='11'] { 979 | -moz-tab-size: 11; 980 | tab-size: 11; 981 | } 982 | 983 | .markdown-body .tab-size[data-tab-size='12'] { 984 | -moz-tab-size: 12; 985 | tab-size: 12; 986 | } 987 | 988 | .markdown-body .task-list-item { 989 | list-style-type: none; 990 | } 991 | 992 | .markdown-body .task-list-item + .task-list-item { 993 | margin-top: 3px; 994 | } 995 | 996 | .markdown-body .task-list-item input { 997 | margin: 0 0.2em 0.25em -1.6em; 998 | vertical-align: middle; 999 | } 1000 | --------------------------------------------------------------------------------