diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..bf2e7648 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +shamefully-hoist=true diff --git a/packages/app/.gitignore b/packages/app/.gitignore new file mode 100644 index 00000000..6dab11aa --- /dev/null +++ b/packages/app/.gitignore @@ -0,0 +1,8 @@ +node_modules +*.log +dist +.output +.nuxt +.env +.idea/ +.data \ No newline at end of file diff --git a/packages/app/LICENSE b/packages/app/LICENSE new file mode 100644 index 00000000..85ea1736 --- /dev/null +++ b/packages/app/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024-PRESENT Sebastien Chopin & Anthony Fu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/app/README.md b/packages/app/README.md new file mode 100644 index 00000000..664d93b1 --- /dev/null +++ b/packages/app/README.md @@ -0,0 +1,72 @@ +# Share your Open Source Contributions 🤍 + +Create a website with an RSS feed of your recent GitHub pull requests across the Open Source projects you contribute to. + +![atinux-pull-requests](https://github.com/user-attachments/assets/cfa82cc2-51af-4fd4-9012-1f8517dd370f) + +Demo: https://prs.atinux.com + +[![Deploy to NuxtHub](https://hub.nuxt.com/button.svg)](https://hub.nuxt.com/new?template=my-pull-requests) + +## Features + +- List the 50 most recent pull requests you've contributed to. +- RSS feed +- Only add your GitHub token to get started +- One click deploy on 275+ locations for free + +## Setup + +Make sure to install the dependencies with [pnpm](https://pnpm.io/installation#using-corepack): + +```bash +pnpm install +``` + +Copy the `.env.example` file to `.env` and fill in your GitHub token: + +```bash +cp .env.example .env +``` + +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: + +```bash +NUXT_GITHUB_TOKEN=your-github-token +``` + +## Development Server + +Start the development server on `http://localhost:3000`: + +```bash +pnpm dev +``` + +## Production + +Build the application for production: + +```bash +pnpm build +``` + +## Deploy + +Deploy the application on the Edge with [NuxtHub](https://hub.nuxt.com) on your Cloudflare account: + +```bash +npx nuxthub deploy +``` + +Then checkout your server cache, analaytics and more in the [NuxtHub Admin](https://admin.hub.nuxt.com). + +You can also deploy using [Cloudflare Pages CI](https://hub.nuxt.com/docs/getting-started/deploy#cloudflare-pages-ci). + +## Credits + +This project is inspired by [Anthony Fu](https://github.com/antfu)'s [releases.antfu.me](https://github.com/antfu/releases.antfu.me) project. + +## License + +[MIT](./LICENSE) diff --git a/packages/app/app/app.config.ts b/packages/app/app/app.config.ts new file mode 100644 index 00000000..36cc1b85 --- /dev/null +++ b/packages/app/app/app.config.ts @@ -0,0 +1,6 @@ +export default defineAppConfig({ + ui: { + primary: 'orange', + gray: 'zinc', + }, +}) diff --git a/packages/app/app/app.vue b/packages/app/app/app.vue new file mode 100644 index 00000000..a29084a0 --- /dev/null +++ b/packages/app/app/app.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/packages/app/app/components/Commits.vue b/packages/app/app/components/Commits.vue new file mode 100644 index 00000000..7b2c4087 --- /dev/null +++ b/packages/app/app/components/Commits.vue @@ -0,0 +1,209 @@ + + + + + diff --git a/packages/app/app/components/RepoButton.vue b/packages/app/app/components/RepoButton.vue new file mode 100644 index 00000000..f15dfe99 --- /dev/null +++ b/packages/app/app/components/RepoButton.vue @@ -0,0 +1,29 @@ + + + diff --git a/packages/app/app/pages/[owner]/[repo].vue b/packages/app/app/pages/[owner]/[repo].vue new file mode 100644 index 00000000..fe62c9bc --- /dev/null +++ b/packages/app/app/pages/[owner]/[repo].vue @@ -0,0 +1,100 @@ + + + diff --git a/packages/app/app/pages/index.client.vue b/packages/app/app/pages/index.client.vue new file mode 100644 index 00000000..4c79ad76 --- /dev/null +++ b/packages/app/app/pages/index.client.vue @@ -0,0 +1,77 @@ + + + diff --git a/packages/app/commits.get.ts b/packages/app/commits.get.ts new file mode 100644 index 00000000..88329908 --- /dev/null +++ b/packages/app/commits.get.ts @@ -0,0 +1,107 @@ +import { z } from 'zod' + +const querySchema = z.object({ + owner: z.string(), + repo: z.string(), + cursor: z.string().optional(), +}) + +export default defineEventHandler(async (event) => { + const query = await getValidatedQuery(event, data => querySchema.parse(data)) + + const { repository } = await useGithubGraphQL().graphql<{ + repository: { + id: string + defaultBranchRef: { + id: string + name: string + target: { + id: string + history: { + nodes: { + id: string + oid: string + abbreviatedOid: string + message: string + authoredDate: string + url: string + statusCheckRollup: { + id: string + state: string + contexts: { + nodes: { + id: string + status: string + name: string + title: string + summary: string + text: string + detailsUrl: string + url: string + }[] + } + } + }[] + pageInfo: { + hasNextPage: boolean + endCursor: string + } + } + } + } + } + }>(` +query ($repoOwner: String!, $repoName: String!, $cursor: String) { + repository (owner: $repoOwner, name: $repoName) { + id + defaultBranchRef { + id + name + target { + id + ...on Commit { + history (first: 10, after: $cursor) { + nodes { + id + oid + abbreviatedOid + message + authoredDate + url + statusCheckRollup { + id + state + contexts (first: 42) { + nodes { + ...on CheckRun { + id + status + name + title + summary + text + detailsUrl + url + } + } + } + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + } +} +`, { + repoOwner: query.owner, + repoName: query.repo, + cursor: query.cursor, + }) + + return repository.defaultBranchRef +}) diff --git a/packages/app/eslint.config.mjs b/packages/app/eslint.config.mjs new file mode 100644 index 00000000..3b3debed --- /dev/null +++ b/packages/app/eslint.config.mjs @@ -0,0 +1,6 @@ +import antfu from '@antfu/eslint-config' +import withNuxt from './.nuxt/eslint.config.mjs' + +export default withNuxt( + antfu(), +) diff --git a/packages/app/favicon.png b/packages/app/favicon.png new file mode 100644 index 00000000..cf5dd20b Binary files /dev/null and b/packages/app/favicon.png differ diff --git a/packages/app/favicon.svg b/packages/app/favicon.svg new file mode 100644 index 00000000..2dcd22a7 --- /dev/null +++ b/packages/app/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/feed.xml.ts b/packages/app/feed.xml.ts new file mode 100644 index 00000000..a05f69f4 --- /dev/null +++ b/packages/app/feed.xml.ts @@ -0,0 +1,35 @@ +import { Feed } from 'feed' +import { joinURL } from 'ufo' +import { getRequestURL } from 'h3' +import type { Contributions } from '~~/types/index' + +export default defineEventHandler(async (event) => { + const domain = getRequestURL(event).origin + const { user, prs } = await $fetch('/api/contributions') + const feed = new Feed({ + title: `${user.name} is contributing...`, + description: `Discover ${user.name}'s recent pull requests on GitHub`, + id: domain, + link: domain, + language: 'en', + image: joinURL(domain, 'favicon.png'), + favicon: joinURL(domain, 'favicon.png'), + copyright: `CC BY-NC-SA 4.0 2024 © ${user.name}`, + feedLinks: { + rss: `${domain}/rss.xml`, + }, + }) + + for (const pr of prs) { + feed.addItem({ + link: pr.url, + date: new Date(pr.created_at), + title: pr.title, + image: `https://github.com/${pr.repo.split('/')[0]}.png`, + description: `${pr.title}`, + }) + } + + appendHeader(event, 'Content-Type', 'application/xml') + return feed.rss2() +}) diff --git a/packages/app/github.ts b/packages/app/github.ts new file mode 100644 index 00000000..cf550aab --- /dev/null +++ b/packages/app/github.ts @@ -0,0 +1,17 @@ +import process from 'node:process' +import { graphql } from '@octokit/graphql' + +let graphQlWithAuth: typeof graphql + +export function useGithubGraphQL() { + if (!graphQlWithAuth) { + graphQlWithAuth = graphql.defaults({ + headers: { + authorization: `token ${process.env.NUXT_GITHUB_TOKEN}`, + }, + }) + } + return { + graphql: graphQlWithAuth, + } +} diff --git a/packages/app/index.get.ts b/packages/app/index.get.ts new file mode 100644 index 00000000..5596c2f3 --- /dev/null +++ b/packages/app/index.get.ts @@ -0,0 +1,52 @@ +import { z } from 'zod' + +const querySchema = z.object({ + owner: z.string(), + repo: z.string(), +}) + +const getRepoInfo = defineCachedFunction(async (owner: string, repo: string) => { + const { repository } = await useGithubGraphQL().graphql<{ + repository: { + id: string + name: string + owner: { + id: string + avatarUrl: string + login: string + } + url: string + homepageUrl: string + description: string + } + }>(` +query ($repoOwner: String!, $repoName: String!) { + repository (owner: $repoOwner, name: $repoName) { + id + name + owner { + id + avatarUrl + login + } + url + homepageUrl + description + } +} +`, { + repoOwner: owner, + repoName: repo, + }) + + return repository +}, { + getKey: (owner: string, repo: string) => `${owner}/${repo}`, + maxAge: 60 * 30, // 5 minutes + swr: true, +}) + +export default defineEventHandler(async (event) => { + const query = await getValidatedQuery(event, data => querySchema.parse(data)) + return getRepoInfo(query.owner, query.repo) +}) diff --git a/packages/app/nuxt.config.ts b/packages/app/nuxt.config.ts new file mode 100644 index 00000000..101c7b8b --- /dev/null +++ b/packages/app/nuxt.config.ts @@ -0,0 +1,24 @@ +// https://nuxt.com/docs/api/configuration/nuxt-config +export default defineNuxtConfig({ + compatibilityDate: '2024-07-30', + + // https://nuxt.com/docs/getting-started/upgrade#testing-nuxt-4 + future: { compatibilityVersion: 4 }, + + // https://nuxt.com/modules + modules: [ + '@nuxt/eslint', + '@nuxt/ui', + '@vueuse/nuxt', + ], + + // https://eslint.nuxt.com + eslint: { + config: { + standalone: false, + }, + }, + + // https://devtools.nuxt.com + devtools: { enabled: true }, +}) diff --git a/packages/app/package.json b/packages/app/package.json new file mode 100644 index 00000000..c3112738 --- /dev/null +++ b/packages/app/package.json @@ -0,0 +1,36 @@ +{ + "type": "module", + "private": true, + "packageManager": "pnpm@9.7.1", + "scripts": { + "build": "nuxi build", + "dev": "nuxi dev", + "generate": "nuxi generate", + "prepare": "nuxi prepare", + "start": "node .output/server/index.mjs", + "start:generate": "npx serve .output/public", + "lint": "eslint .", + "typecheck": "vue-tsc --noEmit" + }, + "dependencies": { + "@iconify-json/ph": "^1.1.14", + "@nuxt/ui": "^2.18.4", + "@octokit/graphql": "^8.1.1", + "@vueuse/core": "^11.0.1", + "@vueuse/nuxt": "^11.0.1", + "feed": "^4.2.2", + "marked": "^14.0.0", + "nuxt": "^3.12.4", + "vue": "^3.4.38", + "vue-router": "^4.4.3", + "zod": "^3.23.8" + }, + "devDependencies": { + "@antfu/eslint-config": "^2.27.0", + "@nuxt/eslint": "^0.5.1", + "eslint": "^9.9.0", + "typescript": "^5.5.4", + "vue-tsc": "^2.0.29", + "wrangler": "^3.72.1" + } +} diff --git a/packages/app/public/favicon.png b/packages/app/public/favicon.png new file mode 100644 index 00000000..cf5dd20b Binary files /dev/null and b/packages/app/public/favicon.png differ diff --git a/packages/app/public/favicon.svg b/packages/app/public/favicon.svg new file mode 100644 index 00000000..2dcd22a7 --- /dev/null +++ b/packages/app/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app/public/robots.txt b/packages/app/public/robots.txt new file mode 100644 index 00000000..c2a49f4f --- /dev/null +++ b/packages/app/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Allow: / diff --git a/packages/app/repo/commits.get.ts b/packages/app/repo/commits.get.ts new file mode 100644 index 00000000..88329908 --- /dev/null +++ b/packages/app/repo/commits.get.ts @@ -0,0 +1,107 @@ +import { z } from 'zod' + +const querySchema = z.object({ + owner: z.string(), + repo: z.string(), + cursor: z.string().optional(), +}) + +export default defineEventHandler(async (event) => { + const query = await getValidatedQuery(event, data => querySchema.parse(data)) + + const { repository } = await useGithubGraphQL().graphql<{ + repository: { + id: string + defaultBranchRef: { + id: string + name: string + target: { + id: string + history: { + nodes: { + id: string + oid: string + abbreviatedOid: string + message: string + authoredDate: string + url: string + statusCheckRollup: { + id: string + state: string + contexts: { + nodes: { + id: string + status: string + name: string + title: string + summary: string + text: string + detailsUrl: string + url: string + }[] + } + } + }[] + pageInfo: { + hasNextPage: boolean + endCursor: string + } + } + } + } + } + }>(` +query ($repoOwner: String!, $repoName: String!, $cursor: String) { + repository (owner: $repoOwner, name: $repoName) { + id + defaultBranchRef { + id + name + target { + id + ...on Commit { + history (first: 10, after: $cursor) { + nodes { + id + oid + abbreviatedOid + message + authoredDate + url + statusCheckRollup { + id + state + contexts (first: 42) { + nodes { + ...on CheckRun { + id + status + name + title + summary + text + detailsUrl + url + } + } + } + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + } +} +`, { + repoOwner: query.owner, + repoName: query.repo, + cursor: query.cursor, + }) + + return repository.defaultBranchRef +}) diff --git a/packages/app/repo/index.get.ts b/packages/app/repo/index.get.ts new file mode 100644 index 00000000..5596c2f3 --- /dev/null +++ b/packages/app/repo/index.get.ts @@ -0,0 +1,52 @@ +import { z } from 'zod' + +const querySchema = z.object({ + owner: z.string(), + repo: z.string(), +}) + +const getRepoInfo = defineCachedFunction(async (owner: string, repo: string) => { + const { repository } = await useGithubGraphQL().graphql<{ + repository: { + id: string + name: string + owner: { + id: string + avatarUrl: string + login: string + } + url: string + homepageUrl: string + description: string + } + }>(` +query ($repoOwner: String!, $repoName: String!) { + repository (owner: $repoOwner, name: $repoName) { + id + name + owner { + id + avatarUrl + login + } + url + homepageUrl + description + } +} +`, { + repoOwner: owner, + repoName: repo, + }) + + return repository +}, { + getKey: (owner: string, repo: string) => `${owner}/${repo}`, + maxAge: 60 * 30, // 5 minutes + swr: true, +}) + +export default defineEventHandler(async (event) => { + const query = await getValidatedQuery(event, data => querySchema.parse(data)) + return getRepoInfo(query.owner, query.repo) +}) diff --git a/packages/app/repo/search.get.ts b/packages/app/repo/search.get.ts new file mode 100644 index 00000000..4f7d9527 --- /dev/null +++ b/packages/app/repo/search.get.ts @@ -0,0 +1,47 @@ +import { z } from 'zod' + +const querySchema = z.object({ + text: z.string(), +}) + +export default defineEventHandler(async (event) => { + const query = await getValidatedQuery(event, data => querySchema.parse(data)) + + if (!query.text) { + return null + } + + const result = await useGithubGraphQL().graphql<{ + search: { + nodes: { + id: string + name: string + owner: { + id: string + login: string + avatarUrl: string + } + }[] + } + }>(` + query ($text: String!) { + search (type: REPOSITORY, query: $text, first: 10) { + nodes { + ...on Repository { + id + name + owner { + id + login + avatarUrl + } + } + } + } + } + `, { + text: query.text, + }) + + return result.search +}) diff --git a/packages/app/robots.txt b/packages/app/robots.txt new file mode 100644 index 00000000..c2a49f4f --- /dev/null +++ b/packages/app/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Allow: / diff --git a/packages/app/routes/feed.xml.ts b/packages/app/routes/feed.xml.ts new file mode 100644 index 00000000..a05f69f4 --- /dev/null +++ b/packages/app/routes/feed.xml.ts @@ -0,0 +1,35 @@ +import { Feed } from 'feed' +import { joinURL } from 'ufo' +import { getRequestURL } from 'h3' +import type { Contributions } from '~~/types/index' + +export default defineEventHandler(async (event) => { + const domain = getRequestURL(event).origin + const { user, prs } = await $fetch('/api/contributions') + const feed = new Feed({ + title: `${user.name} is contributing...`, + description: `Discover ${user.name}'s recent pull requests on GitHub`, + id: domain, + link: domain, + language: 'en', + image: joinURL(domain, 'favicon.png'), + favicon: joinURL(domain, 'favicon.png'), + copyright: `CC BY-NC-SA 4.0 2024 © ${user.name}`, + feedLinks: { + rss: `${domain}/rss.xml`, + }, + }) + + for (const pr of prs) { + feed.addItem({ + link: pr.url, + date: new Date(pr.created_at), + title: pr.title, + image: `https://github.com/${pr.repo.split('/')[0]}.png`, + description: `${pr.title}`, + }) + } + + appendHeader(event, 'Content-Type', 'application/xml') + return feed.rss2() +}) diff --git a/packages/app/search.get.ts b/packages/app/search.get.ts new file mode 100644 index 00000000..4f7d9527 --- /dev/null +++ b/packages/app/search.get.ts @@ -0,0 +1,47 @@ +import { z } from 'zod' + +const querySchema = z.object({ + text: z.string(), +}) + +export default defineEventHandler(async (event) => { + const query = await getValidatedQuery(event, data => querySchema.parse(data)) + + if (!query.text) { + return null + } + + const result = await useGithubGraphQL().graphql<{ + search: { + nodes: { + id: string + name: string + owner: { + id: string + login: string + avatarUrl: string + } + }[] + } + }>(` + query ($text: String!) { + search (type: REPOSITORY, query: $text, first: 10) { + nodes { + ...on Repository { + id + name + owner { + id + login + avatarUrl + } + } + } + } + } + `, { + text: query.text, + }) + + return result.search +}) diff --git a/packages/app/server/api/repo/commits.get.ts b/packages/app/server/api/repo/commits.get.ts new file mode 100644 index 00000000..88329908 --- /dev/null +++ b/packages/app/server/api/repo/commits.get.ts @@ -0,0 +1,107 @@ +import { z } from 'zod' + +const querySchema = z.object({ + owner: z.string(), + repo: z.string(), + cursor: z.string().optional(), +}) + +export default defineEventHandler(async (event) => { + const query = await getValidatedQuery(event, data => querySchema.parse(data)) + + const { repository } = await useGithubGraphQL().graphql<{ + repository: { + id: string + defaultBranchRef: { + id: string + name: string + target: { + id: string + history: { + nodes: { + id: string + oid: string + abbreviatedOid: string + message: string + authoredDate: string + url: string + statusCheckRollup: { + id: string + state: string + contexts: { + nodes: { + id: string + status: string + name: string + title: string + summary: string + text: string + detailsUrl: string + url: string + }[] + } + } + }[] + pageInfo: { + hasNextPage: boolean + endCursor: string + } + } + } + } + } + }>(` +query ($repoOwner: String!, $repoName: String!, $cursor: String) { + repository (owner: $repoOwner, name: $repoName) { + id + defaultBranchRef { + id + name + target { + id + ...on Commit { + history (first: 10, after: $cursor) { + nodes { + id + oid + abbreviatedOid + message + authoredDate + url + statusCheckRollup { + id + state + contexts (first: 42) { + nodes { + ...on CheckRun { + id + status + name + title + summary + text + detailsUrl + url + } + } + } + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + } +} +`, { + repoOwner: query.owner, + repoName: query.repo, + cursor: query.cursor, + }) + + return repository.defaultBranchRef +}) diff --git a/packages/app/server/api/repo/index.get.ts b/packages/app/server/api/repo/index.get.ts new file mode 100644 index 00000000..5596c2f3 --- /dev/null +++ b/packages/app/server/api/repo/index.get.ts @@ -0,0 +1,52 @@ +import { z } from 'zod' + +const querySchema = z.object({ + owner: z.string(), + repo: z.string(), +}) + +const getRepoInfo = defineCachedFunction(async (owner: string, repo: string) => { + const { repository } = await useGithubGraphQL().graphql<{ + repository: { + id: string + name: string + owner: { + id: string + avatarUrl: string + login: string + } + url: string + homepageUrl: string + description: string + } + }>(` +query ($repoOwner: String!, $repoName: String!) { + repository (owner: $repoOwner, name: $repoName) { + id + name + owner { + id + avatarUrl + login + } + url + homepageUrl + description + } +} +`, { + repoOwner: owner, + repoName: repo, + }) + + return repository +}, { + getKey: (owner: string, repo: string) => `${owner}/${repo}`, + maxAge: 60 * 30, // 5 minutes + swr: true, +}) + +export default defineEventHandler(async (event) => { + const query = await getValidatedQuery(event, data => querySchema.parse(data)) + return getRepoInfo(query.owner, query.repo) +}) diff --git a/packages/app/server/api/repo/search.get.ts b/packages/app/server/api/repo/search.get.ts new file mode 100644 index 00000000..4f7d9527 --- /dev/null +++ b/packages/app/server/api/repo/search.get.ts @@ -0,0 +1,47 @@ +import { z } from 'zod' + +const querySchema = z.object({ + text: z.string(), +}) + +export default defineEventHandler(async (event) => { + const query = await getValidatedQuery(event, data => querySchema.parse(data)) + + if (!query.text) { + return null + } + + const result = await useGithubGraphQL().graphql<{ + search: { + nodes: { + id: string + name: string + owner: { + id: string + login: string + avatarUrl: string + } + }[] + } + }>(` + query ($text: String!) { + search (type: REPOSITORY, query: $text, first: 10) { + nodes { + ...on Repository { + id + name + owner { + id + login + avatarUrl + } + } + } + } + } + `, { + text: query.text, + }) + + return result.search +}) diff --git a/packages/app/server/routes/feed.xml.ts b/packages/app/server/routes/feed.xml.ts new file mode 100644 index 00000000..a05f69f4 --- /dev/null +++ b/packages/app/server/routes/feed.xml.ts @@ -0,0 +1,35 @@ +import { Feed } from 'feed' +import { joinURL } from 'ufo' +import { getRequestURL } from 'h3' +import type { Contributions } from '~~/types/index' + +export default defineEventHandler(async (event) => { + const domain = getRequestURL(event).origin + const { user, prs } = await $fetch('/api/contributions') + const feed = new Feed({ + title: `${user.name} is contributing...`, + description: `Discover ${user.name}'s recent pull requests on GitHub`, + id: domain, + link: domain, + language: 'en', + image: joinURL(domain, 'favicon.png'), + favicon: joinURL(domain, 'favicon.png'), + copyright: `CC BY-NC-SA 4.0 2024 © ${user.name}`, + feedLinks: { + rss: `${domain}/rss.xml`, + }, + }) + + for (const pr of prs) { + feed.addItem({ + link: pr.url, + date: new Date(pr.created_at), + title: pr.title, + image: `https://github.com/${pr.repo.split('/')[0]}.png`, + description: `${pr.title}`, + }) + } + + appendHeader(event, 'Content-Type', 'application/xml') + return feed.rss2() +}) diff --git a/packages/app/server/tsconfig.json b/packages/app/server/tsconfig.json new file mode 100644 index 00000000..b9ed69c1 --- /dev/null +++ b/packages/app/server/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../.nuxt/tsconfig.server.json" +} diff --git a/packages/app/server/utils/github.ts b/packages/app/server/utils/github.ts new file mode 100644 index 00000000..cf550aab --- /dev/null +++ b/packages/app/server/utils/github.ts @@ -0,0 +1,17 @@ +import process from 'node:process' +import { graphql } from '@octokit/graphql' + +let graphQlWithAuth: typeof graphql + +export function useGithubGraphQL() { + if (!graphQlWithAuth) { + graphQlWithAuth = graphql.defaults({ + headers: { + authorization: `token ${process.env.NUXT_GITHUB_TOKEN}`, + }, + }) + } + return { + graphql: graphQlWithAuth, + } +} diff --git a/packages/app/tailwind.config.js b/packages/app/tailwind.config.js new file mode 100644 index 00000000..c189a4a5 --- /dev/null +++ b/packages/app/tailwind.config.js @@ -0,0 +1,9 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [], + theme: { + extend: {}, + }, + plugins: [], +} + diff --git a/packages/app/tsconfig.json b/packages/app/tsconfig.json new file mode 100644 index 00000000..4b34df15 --- /dev/null +++ b/packages/app/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./.nuxt/tsconfig.json" +} diff --git a/packages/app/types/index.ts b/packages/app/types/index.ts new file mode 100644 index 00000000..bbc03d88 --- /dev/null +++ b/packages/app/types/index.ts @@ -0,0 +1,21 @@ +export interface User { + username: string + name: string + avatar: string +} + +export interface PullRequest { + repo: string + title: string + url: string + created_at: string + state: 'merged' | 'open' | 'closed' + number: number + type: 'User' | 'Organization' + stars: number +} + +export interface Contributions { + user: User + prs: PullRequest[] +} diff --git a/packages/app/utils/github.ts b/packages/app/utils/github.ts new file mode 100644 index 00000000..cf550aab --- /dev/null +++ b/packages/app/utils/github.ts @@ -0,0 +1,17 @@ +import process from 'node:process' +import { graphql } from '@octokit/graphql' + +let graphQlWithAuth: typeof graphql + +export function useGithubGraphQL() { + if (!graphQlWithAuth) { + graphQlWithAuth = graphql.defaults({ + headers: { + authorization: `token ${process.env.NUXT_GITHUB_TOKEN}`, + }, + }) + } + return { + graphql: graphQlWithAuth, + } +}