├── .env.example ├── .github └── workflows │ └── nuxthub.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── app ├── app.config.ts ├── app.vue ├── components │ ├── PullRequest.vue │ └── ScrollToTop.vue └── pages │ └── index.vue ├── eslint.config.mjs ├── nuxt.config.ts ├── package.json ├── pnpm-lock.yaml ├── public ├── favicon.png ├── favicon.svg ├── og.png └── robots.txt ├── server ├── api │ └── contributions.ts ├── routes │ └── feed.xml.ts ├── tsconfig.json └── utils │ └── github.ts ├── tsconfig.json └── types └── index.ts /.env.example: -------------------------------------------------------------------------------- 1 | # Create a GitHub token with no special scopes on https://github.com/settings/personal-access-tokens/new 2 | NUXT_GITHUB_TOKEN= 3 | -------------------------------------------------------------------------------- /.github/workflows/nuxthub.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to NuxtHub 2 | on: push 3 | 4 | jobs: 5 | deploy: 6 | name: "Deploy to NuxtHub" 7 | runs-on: ubuntu-latest 8 | environment: 9 | name: ${{ github.ref == 'refs/heads/main' && 'production' || 'preview' }} 10 | url: ${{ steps.deploy.outputs.deployment-url }} 11 | permissions: 12 | contents: read 13 | id-token: write 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Install pnpm 19 | uses: pnpm/action-setup@v4 20 | 21 | - name: Install Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: 22 25 | cache: 'pnpm' 26 | 27 | - name: Install dependencies 28 | run: pnpm install 29 | 30 | - name: Build application 31 | run: pnpm build 32 | 33 | - name: Deploy to NuxtHub 34 | uses: nuxt-hub/action@v1 35 | id: deploy 36 | with: 37 | project-key: my-pull-request-rn5e 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | dist 4 | .output 5 | .nuxt 6 | .env 7 | .idea/ 8 | .data -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.useFlatConfig": true, 3 | "prettier.enable": false, 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024-PRESENT Sebastien Chopin & Anthony Fu 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Showcase your Open Source Contributions 🤍 2 | 3 | Create a website with an RSS feed of your recent GitHub pull requests across the Open Source projects you contribute to. 4 | 5 | ![atinux-pull-requests](https://github.com/user-attachments/assets/cfa82cc2-51af-4fd4-9012-1f8517dd370f) 6 | 7 | Demo: https://prs.atinux.com 8 | 9 | [![Deploy to NuxtHub](https://hub.nuxt.com/button.svg)](https://hub.nuxt.com/new?template=my-pull-requests) 10 | 11 | ## Features 12 | 13 | - List the 50 most recent pull requests you've contributed to. 14 | - RSS feed 15 | - Only add your GitHub token to get started 16 | - One click deploy on 275+ locations for free 17 | 18 | ## Setup 19 | 20 | Make sure to install the dependencies with [pnpm](https://pnpm.io/installation#using-corepack): 21 | 22 | ```bash 23 | pnpm install 24 | ``` 25 | 26 | Copy the `.env.example` file to `.env` and fill in your GitHub token: 27 | 28 | ```bash 29 | cp .env.example .env 30 | ``` 31 | 32 | Create a GitHub token with no special scope on [GitHub](https://github.com/settings/personal-access-tokens/new) and set it in the `.env` file: 33 | 34 | ```bash 35 | NUXT_GITHUB_TOKEN=your-github-token 36 | ``` 37 | 38 | ## Development Server 39 | 40 | Start the development server on `http://localhost:3000`: 41 | 42 | ```bash 43 | pnpm dev 44 | ``` 45 | 46 | ## Production 47 | 48 | Build the application for production: 49 | 50 | ```bash 51 | pnpm build 52 | ``` 53 | 54 | ## Deploy 55 | 56 | Deploy the application on the Edge with [NuxtHub](https://hub.nuxt.com) on your Cloudflare account: 57 | 58 | ```bash 59 | npx nuxthub deploy 60 | ``` 61 | 62 | Then checkout your server cache, analaytics and more in the [NuxtHub Admin](https://admin.hub.nuxt.com). 63 | 64 | You can also deploy using [Cloudflare Pages CI](https://hub.nuxt.com/docs/getting-started/deploy#cloudflare-pages-ci). 65 | 66 | ## Credits 67 | 68 | This project is inspired by [Anthony Fu](https://github.com/antfu)'s [releases.antfu.me](https://github.com/antfu/releases.antfu.me) project. 69 | 70 | ## License 71 | 72 | [MIT](./LICENSE) 73 | -------------------------------------------------------------------------------- /app/app.config.ts: -------------------------------------------------------------------------------- 1 | export default defineAppConfig({ 2 | ui: { 3 | gray: 'zinc', 4 | }, 5 | }) 6 | -------------------------------------------------------------------------------- /app/app.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 24 | 25 | 30 | -------------------------------------------------------------------------------- /app/components/PullRequest.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 74 | -------------------------------------------------------------------------------- /app/components/ScrollToTop.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 24 | -------------------------------------------------------------------------------- /app/pages/index.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 93 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import withNuxt from './.nuxt/eslint.config.mjs' 3 | 4 | export default withNuxt( 5 | // Your custom configs here 6 | ).overrideRules({ 7 | 'vue/max-attributes-per-line': ['warn', { singleline: 3 }], 8 | }) 9 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | // https://nuxt.com/docs/api/configuration/nuxt-config 2 | export default defineNuxtConfig({ 3 | 4 | // https://nuxt.com/modules 5 | modules: [ 6 | '@nuxthub/core', 7 | '@nuxt/eslint', 8 | '@nuxt/ui', 9 | '@vueuse/nuxt', 10 | ], 11 | 12 | // https://devtools.nuxt.com 13 | devtools: { enabled: true }, 14 | 15 | // https://nuxt.com/docs/getting-started/upgrade#testing-nuxt-4 16 | future: { compatibilityVersion: 4 }, 17 | compatibilityDate: '2024-07-30', 18 | 19 | // https://hub.nuxt.com/docs/getting-started/installation#options 20 | hub: { 21 | cache: true, 22 | }, 23 | 24 | // https://eslint.nuxt.com 25 | eslint: { 26 | config: { 27 | stylistic: { 28 | quotes: 'single', 29 | }, 30 | }, 31 | }, 32 | }) 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "private": true, 4 | "packageManager": "pnpm@9.12.3", 5 | "scripts": { 6 | "build": "nuxi build", 7 | "dev": "nuxi dev", 8 | "generate": "nuxi generate", 9 | "prepare": "nuxi prepare", 10 | "start": "node .output/server/index.mjs", 11 | "start:generate": "npx serve .output/public", 12 | "lint": "eslint .", 13 | "typecheck": "vue-tsc --noEmit" 14 | }, 15 | "dependencies": { 16 | "@iconify-json/lucide": "^1.2.14", 17 | "@nuxt/ui": "^2.19.2", 18 | "@nuxthub/core": "^0.8.7", 19 | "@vueuse/core": "^11.2.0", 20 | "@vueuse/nuxt": "^11.2.0", 21 | "feed": "^4.2.2", 22 | "nuxt": "^3.14.159", 23 | "octokit": "^4.0.2", 24 | "vue": "^3.5.12", 25 | "vue-router": "^4.4.5" 26 | }, 27 | "devDependencies": { 28 | "@nuxt/eslint": "^0.6.1", 29 | "eslint": "^9.14.0", 30 | "vue-tsc": "^2.1.10", 31 | "wrangler": "^3.86.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atinux/my-pull-requests/038118fe3ee2f2aa683e67605afd29826cc2e537/public/favicon.png -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atinux/my-pull-requests/038118fe3ee2f2aa683e67605afd29826cc2e537/public/og.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | -------------------------------------------------------------------------------- /server/api/contributions.ts: -------------------------------------------------------------------------------- 1 | import type { Contributions, PullRequest, User } from '~~/types/index' 2 | 3 | export default defineCachedEventHandler(async (event) => { 4 | const octokit = useOctokit() 5 | // Fetch user from token 6 | const userResponse = await octokit.request('GET /user') 7 | const user: User = { 8 | name: userResponse.data.name ?? userResponse.data.login, 9 | username: userResponse.data.login, 10 | avatar: userResponse.data.avatar_url, 11 | } 12 | // Fetch pull requests from user 13 | const { data } = await octokit.request('GET /search/issues', { 14 | // To exclude the pull requests to your repositories 15 | // q: `type:pr+author:"${user.username}"+-user:"${user.username}"`, 16 | // To include the pull requests to your repositories 17 | q: `type:pr+author:"${user.username}"`, 18 | per_page: 50, 19 | page: 1, 20 | }) 21 | 22 | // Filter out closed PRs that are not merged 23 | const filteredPrs = data.items.filter(pr => !(pr.state === 'closed' && !pr.pull_request?.merged_at)) 24 | 25 | const prs: PullRequest[] = [] 26 | // For each PR, fetch the repository details 27 | for (const pr of filteredPrs) { 28 | const [owner, name] = pr.repository_url.split('/').slice(-2) 29 | const repo = await fetchRepo(event, owner!, name!) 30 | 31 | prs.push({ 32 | repo: `${owner}/${name}`, 33 | title: pr.title, 34 | url: pr.html_url, 35 | created_at: pr.created_at, 36 | state: pr.pull_request?.merged_at ? 'merged' : pr.draft ? 'draft' : pr.state as 'open' | 'closed', 37 | number: pr.number, 38 | type: repo.owner.type, // Add type information (User or Organization) 39 | stars: repo.stargazers_count, 40 | }) 41 | } 42 | 43 | return { 44 | user, 45 | prs, 46 | } as Contributions 47 | }, { 48 | group: 'api', 49 | name: 'contributions', 50 | getKey: () => 'all', 51 | swr: true, 52 | maxAge: 60 * 5, // 5 minutes 53 | }) 54 | -------------------------------------------------------------------------------- /server/routes/feed.xml.ts: -------------------------------------------------------------------------------- 1 | import { Feed } from 'feed' 2 | import { joinURL } from 'ufo' 3 | import { getRequestURL } from 'h3' 4 | import type { Contributions } from '~~/types/index' 5 | 6 | export default defineEventHandler(async (event) => { 7 | const domain = getRequestURL(event).origin 8 | const { user, prs } = await $fetch('/api/contributions') 9 | const feed = new Feed({ 10 | title: `${user.name} is contributing...`, 11 | description: `Discover ${user.name}'s recent pull requests on GitHub`, 12 | id: domain, 13 | link: domain, 14 | language: 'en', 15 | image: joinURL(domain, 'favicon.png'), 16 | favicon: joinURL(domain, 'favicon.png'), 17 | copyright: `CC BY-NC-SA 4.0 2024 © ${user.name}`, 18 | feedLinks: { 19 | rss: `${domain}/rss.xml`, 20 | }, 21 | }) 22 | 23 | for (const pr of prs) { 24 | feed.addItem({ 25 | link: pr.url, 26 | date: new Date(pr.created_at), 27 | title: pr.title, 28 | image: `https://github.com/${pr.repo.split('/')[0]}.png`, 29 | description: `${pr.title}`, 30 | }) 31 | } 32 | 33 | appendHeader(event, 'Content-Type', 'application/xml') 34 | return feed.rss2() 35 | }) 36 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.nuxt/tsconfig.server.json" 3 | } 4 | -------------------------------------------------------------------------------- /server/utils/github.ts: -------------------------------------------------------------------------------- 1 | import type { H3Event } from 'h3' 2 | import { Octokit } from 'octokit' 3 | 4 | let _octokit: Octokit 5 | 6 | export function useOctokit() { 7 | if (!_octokit) { 8 | _octokit = new Octokit({ 9 | auth: process.env.NUXT_GITHUB_TOKEN, 10 | }) 11 | } 12 | return _octokit 13 | } 14 | 15 | // Read more about caching functions https://hub.nuxt.com/docs/features/cache#server-functions-caching 16 | export const fetchRepo = defineCachedFunction(async (event: H3Event, owner: string, name: string) => { 17 | // Fetch repository details to get owner type 18 | console.log(`Fetching repository details for ${owner}/${name}`) 19 | const { data } = await useOctokit().request('GET /repos/{owner}/{name}', { 20 | owner, 21 | name, 22 | }) 23 | 24 | return data 25 | }, { 26 | maxAge: 60 * 10, // 10 minutes 27 | swr: true, 28 | group: 'functions', 29 | name: 'getRepoDetails', 30 | getKey: (_event: H3Event, owner: string, repo: string) => `${owner}/${repo}`, 31 | }) 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /types/index.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | username: string 3 | name: string 4 | avatar: string 5 | } 6 | 7 | export interface PullRequest { 8 | repo: string 9 | title: string 10 | url: string 11 | created_at: string 12 | state: 'merged' | 'draft' | 'open' | 'closed' 13 | number: number 14 | type: 'User' | 'Organization' 15 | stars: number 16 | } 17 | 18 | export interface Contributions { 19 | user: User 20 | prs: PullRequest[] 21 | } 22 | --------------------------------------------------------------------------------