From d4e6505600c62c995449a00072260682131360fa Mon Sep 17 00:00:00 2001 From: Pavlenex <36959754+pavlenex@users.noreply.github.com> Date: Mon, 16 Mar 2026 19:51:54 +0100 Subject: [PATCH] refactor user flow and add new contribution paths --- index.html | 10 +- package-lock.json | 18 +- public/data/issues.json | 320 ++++++++++++++++++---------- scripts/fetch-issues.js | 222 +++++++++++++++++-- scripts/skill-mapper.js | 48 ----- src/App.tsx | 59 +++-- src/components/FilterBar.tsx | 113 ++++------ src/components/Hero.tsx | 116 ++++++++-- src/components/IssueCard.tsx | 12 +- src/components/IssueModal.tsx | 19 -- src/components/Navbar.tsx | 91 +++++++- src/components/ResourcesSection.tsx | 296 +++++++++++++++++-------- src/hooks/useFilters.ts | 47 +--- src/hooks/useIssues.ts | 14 +- src/lib/filter-engine.ts | 47 +--- src/lib/skill-map.ts | 32 --- src/lib/utils.ts | 6 +- src/types/index.ts | 10 +- 18 files changed, 941 insertions(+), 539 deletions(-) delete mode 100644 scripts/skill-mapper.js delete mode 100644 src/lib/skill-map.ts diff --git a/index.html b/index.html index d1a328e..5a9ae2b 100644 --- a/index.html +++ b/index.html @@ -3,21 +3,21 @@ - BTCPay Contribute — Find your first issue - + BTCPay Contribute - Start contributing to Bitcoin + - + - + - + diff --git a/package-lock.json b/package-lock.json index 5696d76..8bb0a20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "btcpay-contribute", "version": "0.0.0", "dependencies": { + "@octokit/rest": "^22.0.1", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tooltip": "^1.2.8", @@ -21,7 +22,6 @@ }, "devDependencies": { "@eslint/js": "^9.39.1", - "@octokit/rest": "^22.0.1", "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.2.1", "@types/node": "^24.10.1", @@ -1064,7 +1064,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 20" @@ -1074,7 +1073,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz", "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", - "dev": true, "license": "MIT", "dependencies": { "@octokit/auth-token": "^6.0.0", @@ -1093,7 +1091,6 @@ "version": "11.0.3", "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.3.tgz", "integrity": "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==", - "dev": true, "license": "MIT", "dependencies": { "@octokit/types": "^16.0.0", @@ -1107,7 +1104,6 @@ "version": "9.0.3", "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz", "integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==", - "dev": true, "license": "MIT", "dependencies": { "@octokit/request": "^10.0.6", @@ -1122,14 +1118,12 @@ "version": "27.0.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==", - "dev": true, "license": "MIT" }, "node_modules/@octokit/plugin-paginate-rest": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz", "integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==", - "dev": true, "license": "MIT", "dependencies": { "@octokit/types": "^16.0.0" @@ -1145,7 +1139,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz", "integrity": "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==", - "dev": true, "license": "MIT", "engines": { "node": ">= 20" @@ -1158,7 +1151,6 @@ "version": "17.0.0", "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz", "integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==", - "dev": true, "license": "MIT", "dependencies": { "@octokit/types": "^16.0.0" @@ -1174,7 +1166,6 @@ "version": "10.0.8", "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.8.tgz", "integrity": "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw==", - "dev": true, "license": "MIT", "dependencies": { "@octokit/endpoint": "^11.0.3", @@ -1192,7 +1183,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz", "integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==", - "dev": true, "license": "MIT", "dependencies": { "@octokit/types": "^16.0.0" @@ -1205,7 +1195,6 @@ "version": "22.0.1", "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-22.0.1.tgz", "integrity": "sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw==", - "dev": true, "license": "MIT", "dependencies": { "@octokit/core": "^7.0.6", @@ -1221,7 +1210,6 @@ "version": "16.0.0", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz", "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==", - "dev": true, "license": "MIT", "dependencies": { "@octokit/openapi-types": "^27.0.0" @@ -2974,7 +2962,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", - "dev": true, "license": "Apache-2.0" }, "node_modules/brace-expansion": { @@ -3584,7 +3571,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==", - "dev": true, "funding": [ { "type": "github", @@ -4028,7 +4014,6 @@ "version": "3.5.7", "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.7.tgz", "integrity": "sha512-7ei3MdAI5+fJPVnKlW77TKNKwQ5ppSzWvhPuSuINT/GYW9ZOC1eRKOuhV9yHG5aEsUPj9BBx5JIekkmoLHxZOw==", - "dev": true, "license": "MIT" }, "node_modules/json5": { @@ -5817,7 +5802,6 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", - "dev": true, "license": "ISC" }, "node_modules/update-browserslist-db": { diff --git a/public/data/issues.json b/public/data/issues.json index 3b096bc..6c3c54e 100644 --- a/public/data/issues.json +++ b/public/data/issues.json @@ -1,7 +1,7 @@ { - "lastUpdated": "2026-03-16T12:55:36.956Z", - "totalIssues": 14, - "repoCount": 5, + "lastUpdated": "2026-03-16T18:31:59.173Z", + "totalIssues": 15, + "repoCount": 6, "repos": [ { "id": 100711978, @@ -66,12 +66,57 @@ "language": "HTML", "topics": [], "stars": 23 + }, + { + "id": 1179862494, + "name": "btcpay-contribute", + "fullName": "btcpayserver/btcpay-contribute", + "description": "Website that guides new contributors to contribute to Bitcoin and BTCPay Server", + "url": "https://github.com/btcpayserver/btcpay-contribute", + "language": "TypeScript", + "topics": [], + "stars": 1 } ], "issues": [ + { + "id": 4084024455, + "number": 4, + "type": "issue", + "title": "User test the website", + "body": "", + "url": "https://github.com/btcpayserver/btcpay-contribute/issues/4", + "createdAt": "2026-03-16T18:13:10Z", + "updatedAt": "2026-03-16T18:13:15Z", + "commentsCount": 0, + "reactionCount": 0, + "labels": [ + { + "name": "good first issue", + "color": "7057ff" + }, + { + "name": "User testing", + "color": "82e5ea" + } + ], + "repo": { + "name": "btcpay-contribute", + "fullName": "btcpayserver/btcpay-contribute", + "language": "TypeScript", + "url": "https://github.com/btcpayserver/btcpay-contribute" + }, + "assignees": [], + "author": { + "login": "pavlenex", + "avatarUrl": "https://avatars.githubusercontent.com/u/36959754?v=4", + "url": "https://github.com/pavlenex" + } + }, { "id": 4081311843, "number": 7246, + "type": "issue", "title": "[Bug]: store dashboard: wallet balance switch broken", "body": "### What is your BTCPay version?\n\nBTCPay Server v2.3.6 (at least also in 2.3.5)\n\n### How did you deploy BTCPay Server?\n\ndocker\n\n### What happened?\n\nOn the store dashboard wallet balance we have a toggle between 1W 1M and 1Y. When you switch to 1W or 1Y it always shows 21. Jan, 21. Jan on the timeline. Also when you click back to 1W it does not update the screen to show March dates again but stays broken. Only reload fixes screen display.\n\nThis also happens on stores with more transactions than what I have in the screenshots below.\n\nImportant: the graphic updates just fine, only the x-axis date", "url": "https://github.com/btcpayserver/btcpayserver/issues/7246", @@ -96,17 +141,12 @@ "login": "ndeet", "avatarUrl": "https://avatars.githubusercontent.com/u/1136761?v=4", "url": "https://github.com/ndeet" - }, - "skills": [ - "developer" - ], - "tags": [ - "C#" - ] + } }, { "id": 4077245413, "number": 545, + "type": "issue", "title": "New entry submission - Way of Bitcoin", "body": "New submission:\n\nName: Way of Bitcoin\nUrl: https://wayofbitcoin.com\nTwitter: @alanbwt\nType: merchants\nSubType: books\nDescription: A philosophical guide to Bitcoin as a discipline and a way of life. Published by Hyperborean Press. Pay on-chain or with lightning via BTCPay Server Shopify V2.", "url": "https://github.com/btcpayserver/directory.btcpayserver.org/issues/545", @@ -135,17 +175,12 @@ "login": "alanbwt", "avatarUrl": "https://avatars.githubusercontent.com/u/167029568?v=4", "url": "https://github.com/alanbwt" - }, - "skills": [ - "developer" - ], - "tags": [ - "TypeScript" - ] + } }, { "id": 4067414699, "number": 166, + "type": "issue", "title": "[Feature] Cleanup stale unconfirmed users", "body": "## Description \n\nAdd a cleanup job similar to the existing stale plugin cleanup, but for users who registered and never confirmed their email.\n\nScope:\n- delete only users with `EmailConfirmed = false`\n- only delete users older than a defined threshold\n- never delete users with roles, plugin ownership, reviews, or other linked data\n- run on a schedule, like the current plugin cleanup\n- add tests for delete vs keep scenarios\n\nNote:\nAspNetUsers currently does not seem to store account creation date, so this likely needs a CreatedAt field first to define what is stale safely.", "url": "https://github.com/btcpayserver/btcpayserver-plugin-builder/issues/166", @@ -170,17 +205,12 @@ "login": "thgO-O", "avatarUrl": "https://avatars.githubusercontent.com/u/107907441?v=4", "url": "https://github.com/thgO-O" - }, - "skills": [ - "developer" - ], - "tags": [ - "C#" - ] + } }, { "id": 4054026700, "number": 542, + "type": "issue", "title": "New entry submission - MAGIC Grants", "body": "New submission:\n\nName: MAGIC Grants\nUrl: https://donate.magicgrants.org\nTwitter: @MagicGrants\nType: non-profits\nDescription: MAGIC Grants provides scholarships for students interested in cryptocurrencies and privacy, supports public cryptocurrency infrastructure, and promotes privacy.", "url": "https://github.com/btcpayserver/directory.btcpayserver.org/issues/542", @@ -209,23 +239,18 @@ "login": "SamsungGalaxyPlayer", "avatarUrl": "https://avatars.githubusercontent.com/u/12520755?v=4", "url": "https://github.com/SamsungGalaxyPlayer" - }, - "skills": [ - "developer" - ], - "tags": [ - "TypeScript" - ] + } }, { "id": 4046354134, "number": 163, + "type": "issue", "title": "[Feature] Add health check endpoint to Plugin Builder", "body": "Plugin Builder currently has no dedicated health check endpoint.\n\nSince startup depends on Postgres, Docker, and Azure Storage, it would be useful to expose a simple probe endpoint for deployments and monitoring.\n\nSuggested scope:\n- add `/health`\n- return `200 OK` when the app is ready to serve requests\n- return `503` when startup is incomplete or a critical dependency is unavailable\n- keep the check lightweight", "url": "https://github.com/btcpayserver/btcpayserver-plugin-builder/issues/163", "createdAt": "2026-03-09T15:44:59Z", - "updatedAt": "2026-03-12T22:50:51Z", - "commentsCount": 8, + "updatedAt": "2026-03-16T15:25:25Z", + "commentsCount": 9, "reactionCount": 0, "labels": [ { @@ -239,28 +264,17 @@ "language": "C#", "url": "https://github.com/btcpayserver/btcpayserver-plugin-builder" }, - "assignees": [ - { - "login": "Abdullah-Albayati", - "avatarUrl": "https://avatars.githubusercontent.com/u/93524185?v=4", - "url": "https://github.com/Abdullah-Albayati" - } - ], + "assignees": [], "author": { "login": "thgO-O", "avatarUrl": "https://avatars.githubusercontent.com/u/107907441?v=4", "url": "https://github.com/thgO-O" - }, - "skills": [ - "developer" - ], - "tags": [ - "C#" - ] + } }, { "id": 4018455059, "number": 160, + "type": "issue", "title": "Require Video URL filled out in order to request listing in the directory", "body": "Now that @teamssUTXO has [added support for a video URL](https://github.com/btcpayserver/btcpayserver-plugin-builder/pull/150), we should require all newly listed plugins to include an explainer video.\n\nIt does not need to be high production quality - even a basic screen recording of the plugin in action would go a long way in demonstrating that it deserves to be listed.\n\nThe change should be straightforward, likely just an update to this page:\n\n\"Image\"", "url": "https://github.com/btcpayserver/btcpayserver-plugin-builder/issues/160", @@ -285,17 +299,12 @@ "login": "rockstardev", "avatarUrl": "https://avatars.githubusercontent.com/u/5191402?v=4", "url": "https://github.com/rockstardev" - }, - "skills": [ - "developer" - ], - "tags": [ - "C#" - ] + } }, { "id": 3974150655, "number": 158, + "type": "issue", "title": "Port and unify error page system from BTCPayServer (404 and 500 for start)", "body": "It would be great to have a default 404 error page instead of this one :\n\n\"Image\"", "url": "https://github.com/btcpayserver/btcpayserver-plugin-builder/issues/158", @@ -320,17 +329,12 @@ "login": "teamssUTXO", "avatarUrl": "https://avatars.githubusercontent.com/u/183613235?v=4", "url": "https://github.com/teamssUTXO" - }, - "skills": [ - "developer" - ], - "tags": [ - "C#" - ] + } }, { "id": 3820273373, "number": 7109, + "type": "issue", "title": "[Feature]: Improve label system to work when we have 10+ labels", "body": "Now that we are integrating the label system into Payment Requests as well (https://github.com/btcpayserver/btcpayserver/pull/7050), we should improve label UX so it scales to 10, 100, or 1,000 labels.\n\nThe current wallet UI that dumps all labels into the filter dropdown will not scale:\n\nhttps://github.com/user-attachments/assets/a9de89de-b877-472e-a776-921a46da87c9\n\nFrom the video, it is clear the labels filter dropdown should never exceed page height. A good approach would be to reuse the dropdown we already use in transaction rows for applying labels:\n\n\"Ima",\n\n\n**Request:**\nEnhance the Wallet transaction view to allow users to filter transactions by date in the same way as the Payment Requests and Invoices views. This would help users more easily locate relevant transactions and improve consistency across", "url": "https://github.com/btcpayserver/btcpayserver/issues/7092", @@ -404,17 +403,12 @@ "login": "pavlenex", "avatarUrl": "https://avatars.githubusercontent.com/u/36959754?v=4", "url": "https://github.com/pavlenex" - }, - "skills": [ - "developer" - ], - "tags": [ - "C#" - ] + } }, { "id": 3799531574, "number": 7091, + "type": "issue", "title": "Add support for search, specifically for TX id inside the wallet view", "body": "As mentioned in [issue #3954](https://github.com/btcpayserver/btcpayserver/issues/3954), the wallet transactions view should be standardized line invoice or payment requests views and provide a search functionality, allowing users to immediately find a transaction based on criteria such as the transaction ID.\n\n- Implement a search bar in the wallet transactions view so users can filter or rapidly locate transactions using their transaction ID.\n", "url": "https://github.com/btcpayserver/btcpayserver/issues/7091", @@ -443,17 +437,12 @@ "login": "pavlenex", "avatarUrl": "https://avatars.githubusercontent.com/u/36959754?v=4", "url": "https://github.com/pavlenex" - }, - "skills": [ - "developer" - ], - "tags": [ - "C#" - ] + } }, { "id": 3579096842, "number": 1531, + "type": "issue", "title": "Remove Wabisabi Plugin documentation", "body": "This page refers to a plugin not available anymore: https://docs.btcpayserver.org/Wabisabi\n\nAre there any plans to bring the plugin back or should this page better be removed?", "url": "https://github.com/btcpayserver/btcpayserver-doc/issues/1531", @@ -478,17 +467,12 @@ "login": "petzsch", "avatarUrl": "https://avatars.githubusercontent.com/u/1374810?v=4", "url": "https://github.com/petzsch" - }, - "skills": [ - "writer" - ], - "tags": [ - "docs" - ] + } }, { "id": 3051345641, "number": 202, + "type": "issue", "title": "Poor `Pending` status contrast in Light mode > Withdraw > Pending status", "body": "Good first issue for new devs wanting to contribute, pretty straightforward.\n\n\"Image\"\n", "url": "https://github.com/btcpayserver/app/issues/202", @@ -517,17 +501,12 @@ "login": "pavlenex", "avatarUrl": "https://avatars.githubusercontent.com/u/36959754?v=4", "url": "https://github.com/pavlenex" - }, - "skills": [ - "developer" - ], - "tags": [ - "HTML" - ] + } }, { "id": 2950017622, "number": 169, + "type": "issue", "title": "Startup Icon Appears Low Resolution and Pixelated", "body": "The app's startup/splash screen icon appears in low resolution and pixelated when launching the app. \n\nIt may not be visible from the screenshot, but if you deploy on the device, I am sure you can clearly tell.\n\n![Image](https://github.com/user-attachments/assets/105abca3-28fe-4076-928a-cb1e121ab688)", "url": "https://github.com/btcpayserver/app/issues/169", @@ -566,17 +545,12 @@ "login": "pavlenex", "avatarUrl": "https://avatars.githubusercontent.com/u/36959754?v=4", "url": "https://github.com/pavlenex" - }, - "skills": [ - "design" - ], - "tags": [ - "design" - ] + } }, { "id": 389532105, "number": 444, + "type": "issue", "title": "RTL Language Support", "body": "Right-to-Left languages have display issues in the checkout page. Arabic translation is available for anyone who wants to work on this issue. \r\n\r\nOpen RTL Issue to close outdated #305 \r\n", "url": "https://github.com/btcpayserver/btcpayserver/issues/444", @@ -613,13 +587,141 @@ "login": "britttttk", "avatarUrl": "https://avatars.githubusercontent.com/u/39231115?v=4", "url": "https://github.com/britttttk" - }, - "skills": [ - "writer" + } + } + ], + "testerItems": [ + { + "id": 4051110731, + "number": 7226, + "type": "pr", + "title": "show payment method on receipt", + "body": "Resolve #7174 \r\n\r\n\"image\"\r\n\r\n\r\n@dstrukt what do you think?\r\n", + "url": "https://github.com/btcpayserver/btcpayserver/pull/7226", + "createdAt": "2026-03-10T11:22:40Z", + "updatedAt": "2026-03-16T14:39:34Z", + "commentsCount": 3, + "reactionCount": 1, + "labels": [ + { + "name": "User Testing", + "color": "F8ADDA" + } ], - "tags": [ - "docs" - ] + "repo": { + "name": "btcpayserver", + "fullName": "btcpayserver/btcpayserver", + "language": "C#", + "url": "https://github.com/btcpayserver/btcpayserver" + }, + "assignees": [ + { + "login": "dstrukt", + "avatarUrl": "https://avatars.githubusercontent.com/u/6250771?v=4", + "url": "https://github.com/dstrukt" + } + ], + "author": { + "login": "TChukwuleta", + "avatarUrl": "https://avatars.githubusercontent.com/u/47084273?v=4", + "url": "https://github.com/TChukwuleta" + } + }, + { + "id": 3941153156, + "number": 7183, + "type": "pr", + "title": "Refactor navigation: topbar global actions and unified search", + "body": "This PR is a prototype for #6902 \r\n\r\nhttps://github.com/user-attachments/assets/43281d64-b667-448b-ade9-29a0648a8288\r\n\r\nIt adds:\r\n\r\n- Global search (local nav index + server-backexd results).\r\n- Global top-right actions (notifications/account/server operations moved from sidebar context).\r\n- Navigation restructuring to keep store-focused items in the sidebar and global actions in the top bar.\r\n- A dismissible migration hint in the sidebar to guide users to the new top-right location for server/account navigation.\r\n- Mobile navigation adjustments, including moving store selection into the hambu", + "url": "https://github.com/btcpayserver/btcpayserver/pull/7183", + "createdAt": "2026-02-14T11:15:48Z", + "updatedAt": "2026-03-16T14:39:24Z", + "commentsCount": 3, + "reactionCount": 0, + "labels": [ + { + "name": "User Testing", + "color": "F8ADDA" + } + ], + "repo": { + "name": "btcpayserver", + "fullName": "btcpayserver/btcpayserver", + "language": "C#", + "url": "https://github.com/btcpayserver/btcpayserver" + }, + "assignees": [], + "author": { + "login": "pavlenex", + "avatarUrl": "https://avatars.githubusercontent.com/u/36959754?v=4", + "url": "https://github.com/pavlenex" + } + }, + { + "id": 4084024455, + "number": 4, + "type": "issue", + "title": "User test the website", + "body": "", + "url": "https://github.com/btcpayserver/btcpay-contribute/issues/4", + "createdAt": "2026-03-16T18:13:10Z", + "updatedAt": "2026-03-16T18:13:15Z", + "commentsCount": 0, + "reactionCount": 0, + "labels": [ + { + "name": "good first issue", + "color": "7057ff" + }, + { + "name": "User testing", + "color": "82e5ea" + } + ], + "repo": { + "name": "btcpay-contribute", + "fullName": "btcpayserver/btcpay-contribute", + "language": "TypeScript", + "url": "https://github.com/btcpayserver/btcpay-contribute" + }, + "assignees": [], + "author": { + "login": "pavlenex", + "avatarUrl": "https://avatars.githubusercontent.com/u/36959754?v=4", + "url": "https://github.com/pavlenex" + } + } + ], + "writerIssues": [ + { + "id": 3579096842, + "number": 1531, + "type": "issue", + "title": "Remove Wabisabi Plugin documentation", + "body": "This page refers to a plugin not available anymore: https://docs.btcpayserver.org/Wabisabi\n\nAre there any plans to bring the plugin back or should this page better be removed?", + "url": "https://github.com/btcpayserver/btcpayserver-doc/issues/1531", + "createdAt": "2025-11-02T07:59:22Z", + "updatedAt": "2026-03-11T09:03:28Z", + "commentsCount": 1, + "reactionCount": 0, + "labels": [ + { + "name": "good first issue", + "color": "19b542" + } + ], + "repo": { + "name": "btcpayserver-doc", + "fullName": "btcpayserver/btcpayserver-doc", + "language": "Shell", + "url": "https://github.com/btcpayserver/btcpayserver-doc" + }, + "assignees": [], + "author": { + "login": "petzsch", + "avatarUrl": "https://avatars.githubusercontent.com/u/1374810?v=4", + "url": "https://github.com/petzsch" + } } ] } \ No newline at end of file diff --git a/scripts/fetch-issues.js b/scripts/fetch-issues.js index 1487a07..42ac41b 100644 --- a/scripts/fetch-issues.js +++ b/scripts/fetch-issues.js @@ -19,14 +19,15 @@ import { Octokit } from '@octokit/rest' import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs' import { resolve, dirname } from 'path' import { fileURLToPath } from 'url' -import { mapSkills } from './skill-mapper.js' - const __dirname = dirname(fileURLToPath(import.meta.url)) -const ORG = 'btcpayserver' -const LABEL = 'good first issue' -const OUT = resolve(__dirname, '../public/data/issues.json') -const BODY_MAX = 600 +const ORG = 'btcpayserver' +const LABEL = 'good first issue' +const TESTER_LABEL = 'User Testing' +const WRITER_REPOS = ['btcpayserver-doc', 'btcpayserver-blog'] +const COPYWRITING_LABEL = 'copywriting' +const OUT = resolve(__dirname, '../public/data/issues.json') +const BODY_MAX = 600 async function main() { const token = process.env.ORG_GITHUB_TOKEN @@ -70,14 +71,10 @@ async function main() { // Skip pull requests (GitHub returns PRs in issue list) if (raw.pull_request) continue - const { skills, tags } = mapSkills( - { labels: raw.labels.map((l) => (typeof l === 'string' ? { name: l } : l)) }, - { name: repo.name, language: repo.language }, - ) - issues.push({ id: raw.id, number: raw.number, + type: 'issue', title: raw.title, body: (raw.body ?? '').slice(0, BODY_MAX), url: raw.html_url, @@ -104,8 +101,6 @@ async function main() { avatarUrl: raw.user?.avatar_url ?? '', url: raw.user?.html_url ?? '', }, - skills, - tags, }) } @@ -117,6 +112,195 @@ async function main() { // ── 3. Sort by creation date desc ───────────────────────────────────────── issues.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) + // ── 3b. Fetch tester items: open PRs + "User Testing" issues ────────────── + /** @type {any[]} */ + const testerItems = [] + + // PRs and issues labeled "User Testing" across all org repos + console.log(`Fetching "${TESTER_LABEL}" items across org`) + for (const repo of repos) { + let page = 1 + while (true) { + const { data } = await octokit.rest.issues.listForRepo({ + owner: ORG, + repo: repo.name, + labels: TESTER_LABEL, + state: 'open', + per_page: 100, + page, + }) + if (data.length === 0) break + + for (const raw of data) { + const isPR = !!raw.pull_request + testerItems.push({ + id: raw.id, + number: raw.number, + type: isPR ? 'pr' : 'issue', + title: raw.title, + body: (raw.body ?? '').slice(0, BODY_MAX), + url: raw.html_url, + createdAt: raw.created_at, + updatedAt: raw.updated_at ?? raw.created_at, + commentsCount: raw.comments, + reactionCount: raw.reactions?.total_count ?? 0, + labels: raw.labels + .filter((l) => typeof l === 'object') + .map((l) => ({ name: l.name ?? '', color: l.color ?? '888888' })), + repo: { + name: repo.name, + fullName: repo.full_name, + language: repo.language ?? null, + url: repo.html_url, + }, + assignees: (raw.assignees ?? []).map((a) => ({ + login: a.login, + avatarUrl: a.avatar_url, + url: a.html_url, + })), + author: { + login: raw.user?.login ?? 'unknown', + avatarUrl: raw.user?.avatar_url ?? '', + url: raw.user?.html_url ?? '', + }, + }) + } + + if (data.length < 100) break + page++ + } + } + console.log(`Found ${testerItems.filter((i) => i.type === 'pr').length} PRs and ${testerItems.filter((i) => i.type === 'issue').length} issues with "${TESTER_LABEL}" label`) + + // Sort tester items: PRs first, then issues, newest first within each group + testerItems.sort((a, b) => { + if (a.type !== b.type) return a.type === 'pr' ? -1 : 1 + return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + }) + + // ── 3c. Writer issues: all open issues from doc + blog repos ───────────── + /** @type {any[]} */ + const writerIssues = [] + console.log(`Fetching all open issues from writer repos: ${WRITER_REPOS.join(', ')}`) + + for (const repoName of WRITER_REPOS) { + const repo = repos.find((r) => r.name === repoName) + if (!repo) { console.warn(` Writer repo not found: ${repoName}`); continue } + + let page = 1 + while (true) { + const { data } = await octokit.rest.issues.listForRepo({ + owner: ORG, + repo: repoName, + state: 'open', + per_page: 100, + page, + }) + if (data.length === 0) break + + for (const raw of data) { + if (raw.pull_request) continue // skip PRs + writerIssues.push({ + id: raw.id, + number: raw.number, + type: 'issue', + title: raw.title, + body: (raw.body ?? '').slice(0, BODY_MAX), + url: raw.html_url, + createdAt: raw.created_at, + updatedAt: raw.updated_at ?? raw.created_at, + commentsCount: raw.comments, + reactionCount: raw.reactions?.total_count ?? 0, + labels: raw.labels + .filter((l) => typeof l === 'object') + .map((l) => ({ name: l.name ?? '', color: l.color ?? '888888' })), + repo: { + name: repo.name, + fullName: repo.full_name, + language: repo.language ?? null, + url: repo.html_url, + }, + assignees: (raw.assignees ?? []).map((a) => ({ + login: a.login, + avatarUrl: a.avatar_url, + url: a.html_url, + })), + author: { + login: raw.user?.login ?? 'unknown', + avatarUrl: raw.user?.avatar_url ?? '', + url: raw.user?.html_url ?? '', + }, + }) + } + + if (data.length < 100) break + page++ + } + } + + // Also fetch "copywriting" labeled issues across all org repos + const seenWriterIds = new Set(writerIssues.map((i) => i.id)) + console.log(`Fetching "${COPYWRITING_LABEL}" issues across org`) + for (const repo of repos) { + let page = 1 + while (true) { + const { data } = await octokit.rest.issues.listForRepo({ + owner: ORG, + repo: repo.name, + labels: COPYWRITING_LABEL, + state: 'open', + per_page: 100, + page, + }) + if (data.length === 0) break + for (const raw of data) { + if (raw.pull_request) continue + if (seenWriterIds.has(raw.id)) continue // already included + seenWriterIds.add(raw.id) + writerIssues.push({ + id: raw.id, + number: raw.number, + type: 'issue', + title: raw.title, + body: (raw.body ?? '').slice(0, BODY_MAX), + url: raw.html_url, + createdAt: raw.created_at, + updatedAt: raw.updated_at ?? raw.created_at, + commentsCount: raw.comments, + reactionCount: raw.reactions?.total_count ?? 0, + labels: raw.labels + .filter((l) => typeof l === 'object') + .map((l) => ({ name: l.name ?? '', color: l.color ?? '888888' })), + repo: { + name: repo.name, + fullName: repo.full_name, + language: repo.language ?? null, + url: repo.html_url, + }, + assignees: (raw.assignees ?? []).map((a) => ({ + login: a.login, + avatarUrl: a.avatar_url, + url: a.html_url, + })), + author: { + login: raw.user?.login ?? 'unknown', + avatarUrl: raw.user?.avatar_url ?? '', + url: raw.user?.html_url ?? '', + }, + }) + } + if (data.length < 100) break + page++ + } + } + + // Sort writer issues: unassigned first, then newest + writerIssues.sort((a, b) => { + if (a.assignees.length !== b.assignees.length) return a.assignees.length - b.assignees.length + return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + }) + console.log(`Found ${writerIssues.length} open writer issues (doc/blog repos + "${COPYWRITING_LABEL}" label)`) + // ── 4. Build repo list ───────────────────────────────────────────────────── const reposWithIssues = repos .filter((r) => issues.some((i) => i.repo.name === r.name)) @@ -132,11 +316,13 @@ async function main() { })) const output = { - lastUpdated: new Date().toISOString(), - totalIssues: issues.length, - repoCount: reposWithIssues.length, - repos: reposWithIssues, + lastUpdated: new Date().toISOString(), + totalIssues: issues.length, + repoCount: reposWithIssues.length, + repos: reposWithIssues, issues, + testerItems, + writerIssues, } // ── 5. Diff check — skip write if unchanged ──────────────────────────────── @@ -152,7 +338,7 @@ async function main() { mkdirSync(dirname(OUT), { recursive: true }) writeFileSync(OUT, json, 'utf8') - console.log(`✓ Wrote ${issues.length} issues from ${reposWithIssues.length} repos to ${OUT}`) + console.log(`✓ Wrote ${issues.length} issues, ${testerItems.length} tester items, ${writerIssues.length} writer issues from ${reposWithIssues.length} repos to ${OUT}`) } main().catch((err) => { diff --git a/scripts/skill-mapper.js b/scripts/skill-mapper.js deleted file mode 100644 index 34d27c6..0000000 --- a/scripts/skill-mapper.js +++ /dev/null @@ -1,48 +0,0 @@ -// @ts-check - -/** - * Maps a GitHub issue + repo to skill categories. - * - * Rules (an issue can match multiple skills): - * writer — repo is btcpayserver-doc OR label is docs/writing/translation - * design — label is design/ui/ux - * marketing — label is marketing/community/growth/seo - * developer — default when none of the above match - * - * Mirrors src/lib/skill-map.ts — keep both in sync. - */ - -const WRITER_REPOS = new Set(['btcpayserver-doc']) -const WRITER_LABELS = new Set(['writing', 'documentation', 'docs', 'translation', 'content writing']) -const DESIGN_LABELS = new Set(['design', 'ui', 'ux', 'ui/ux']) -const MARKETING_LABELS = new Set(['marketing', 'community', 'growth', 'seo']) - -/** - * @param {{ labels: { name: string }[] }} issue - * @param {{ name: string, language: string | null }} repo - * @returns {{ skills: string[], tags: string[] }} - */ -export function mapSkills(issue, repo) { - const labelNames = issue.labels.map((l) => l.name.toLowerCase()) - - const isWriter = WRITER_REPOS.has(repo.name) || labelNames.some((l) => WRITER_LABELS.has(l)) - const isDesign = labelNames.some((l) => DESIGN_LABELS.has(l)) - const isMarketing = labelNames.some((l) => MARKETING_LABELS.has(l)) - - /** @type {string[]} */ - const skills = [] - if (isWriter) skills.push('writer') - if (isDesign) skills.push('design') - if (isMarketing) skills.push('marketing') - if (skills.length === 0) skills.push('developer') // default - - // Tags: repo language for developer issues - /** @type {string[]} */ - const tags = [] - if (skills.includes('developer') && repo.language) tags.push(repo.language) - if (isWriter) tags.push('docs') - if (isDesign) tags.push('design') - if (isMarketing) tags.push('marketing') - - return { skills, tags } -} diff --git a/src/App.tsx b/src/App.tsx index e0d84c0..59886ef 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { useState, lazy, Suspense } from 'react' +import { useState, lazy, Suspense, useCallback } from 'react' import type React from 'react' import Navbar from '@/components/Navbar' import Hero from '@/components/Hero' @@ -9,40 +9,72 @@ import Footer from '@/components/Footer' const IssueModal = lazy(() => import('@/components/IssueModal')) const preloadIssueModal = () => import('@/components/IssueModal') + import { useFilters } from '@/hooks/useFilters' import { useIssues } from '@/hooks/useIssues' -import type { Issue } from '@/types' +import type { Issue, Role } from '@/types' + +const ROLE_SECTION_TITLE: Record = { + developer: { + heading: 'Jump right in', + sub: 'Pick a good first issue and start shipping.', + }, + tester: { + heading: 'Jump right in', + sub: 'The best testers use BTCPay Server daily. Test PRs, try every release, and report every bug you find.', + }, + writer: { + heading: 'Jump right in', + sub: 'Pick an open issue in the docs or blog and start writing.', + }, +} export default function App() { - const { filters, setSkill, setQuery, clearAll } = useFilters() - const { filtered, status } = useIssues(filters) + const [selectedRole, setSelectedRole] = useState('developer') + + const { filters, setQuery } = useFilters() + const { filtered, testerFiltered, writerFiltered, status } = useIssues(filters) const [selectedIssue, setSelectedIssue] = useState(null) const [slideFrom, setSlideFrom] = useState<'left' | 'bottom' | 'right'>('bottom') + const handleRoleSelect = useCallback((role: Role) => { + setSelectedRole(role) + setQuery('') + }, [setQuery]) + const handleIssueClick = (e: React.MouseEvent, issue: Issue) => { const { left, width } = (e.currentTarget as HTMLElement).getBoundingClientRect() const third = window.innerWidth / 3 const cardCenter = left + width / 2 - const direction = cardCenter < third ? 'left' : cardCenter > third * 2 ? 'right' : 'bottom' - setSlideFrom(direction) setSelectedIssue(issue) } + const { heading, sub } = ROLE_SECTION_TITLE[selectedRole] + + function getIssues() { + if (selectedRole === 'tester') return testerFiltered + if (selectedRole === 'writer') return writerFiltered + return filtered + } + return ( <> - +
- +

- Pick an issue + {heading}

- Filter by skill and find something that excites you. + {sub}

@@ -51,23 +83,20 @@ export default function App() {
- - +