├── 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 |
9 | {console.error(props.error)}
10 | Something went wrong. Check the report details in the console.
11 |
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 |
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 |
13 |
17 |
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 |
12 |
16 |
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 |
13 |
17 |
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 |
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 |
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 | [](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 |
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 |
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 | track('submit', { step: 'preview' })}
45 | href={createGithubIssue(repo)}
46 | >
47 | Submit request
48 |
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 |
14 | {
16 | const element = document.scrollingElement || document.documentElement
17 | if (element) {
18 | element.scrollTop = element.scrollHeight
19 | }
20 | }}
21 | className='flex flex-row items-center p-4 bg-gray-800 rounded shadow-lg'
22 | onTouchStart={() => setIsBeating(true)}
23 | onTouchEnd={() => setIsBeating(false)}
24 | onMouseEnter={() => setIsBeating(true)}
25 | onMouseLeave={() => setIsBeating(false)}
26 | >
27 |
33 |
34 |
38 |
43 |
47 |
51 |
55 |
56 |
57 |
58 |
Don't miss any opportunity
59 |
60 | Join us{' '}
61 | `scale(${x})`),
73 | }}
74 | >
75 | 💖
76 |
77 |
78 |
79 |
80 |
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 | You need to enable JavaScript to run this app.
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------