diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 71ab06e9..87c83f3d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -96,6 +96,18 @@ jobs:
if: github.event_name == 'pull_request' && steps.react-ui-changes.outputs.changed != 'true'
run: echo "Skipping React Doctor because this pull request did not change React UI source."
+ - name: Write React Doctor badge payload
+ if: github.event_name == 'push' && github.ref == 'refs/heads/master'
+ run: node scripts/write-react-doctor-badge.mjs
+
+ - name: Upload React Doctor badge
+ if: github.event_name == 'push' && github.ref == 'refs/heads/master'
+ uses: actions/upload-artifact@v4
+ with:
+ name: react-doctor-badge
+ path: badges/react-doctor.json
+ if-no-files-found: error
+
- name: Install Chromium for smoke tests
run: npx playwright install --with-deps chromium
@@ -111,7 +123,7 @@ jobs:
if-no-files-found: warn
publish-coverage-badge:
- name: Publish Coverage Badge
+ name: Publish Badges
runs-on: ubuntu-22.04
needs: quality
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
@@ -132,13 +144,21 @@ jobs:
name: coverage-badge
path: ${{ runner.temp }}/coverage-badge
+ - name: Download React Doctor badge
+ uses: actions/download-artifact@v4
+ with:
+ name: react-doctor-badge
+ path: ${{ runner.temp }}/react-doctor-badge
+
- name: Prepare Pages artifact
env:
BADGE_SOURCE_PATH: ${{ runner.temp }}/coverage-badge/coverage.json
+ REACT_DOCTOR_BADGE_SOURCE_PATH: ${{ runner.temp }}/react-doctor-badge/react-doctor.json
PAGES_OUTPUT_PATH: ${{ runner.temp }}/github-pages
run: |
mkdir -p "${PAGES_OUTPUT_PATH}/badges"
cp "${BADGE_SOURCE_PATH}" "${PAGES_OUTPUT_PATH}/badges/coverage.json"
+ cp "${REACT_DOCTOR_BADGE_SOURCE_PATH}" "${PAGES_OUTPUT_PATH}/badges/react-doctor.json"
touch "${PAGES_OUTPUT_PATH}/.nojekyll"
- name: Upload Pages artifact
diff --git a/README.md b/README.md
index 92f5a4e9..9315819d 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,6 @@
[](https://github.com/bitsocialnet/5chan/actions/workflows/ci.yml)
[](https://github.com/bitsocialnet/5chan/blob/master/scripts/write-coverage-badge.mjs)
+[](https://github.com/bitsocialnet/5chan/actions/workflows/ci.yml)
[](https://github.com/bitsocialnet/5chan/releases/latest)
[](https://github.com/bitsocialnet/5chan/blob/master/LICENSE)
[](http://commitizen.github.io/cz-cli/)
diff --git a/package.json b/package.json
index 22b268f0..5fa543b1 100644
--- a/package.json
+++ b/package.json
@@ -102,6 +102,7 @@
"doctor": "react-doctor . -y",
"doctor:score": "react-doctor . --score -y",
"doctor:verbose": "react-doctor . --verbose -y",
+ "doctor:badge": "node scripts/write-react-doctor-badge.mjs",
"contract:imgur": "cd android && ./gradlew :app:connectedDebugAndroidTest -Pandroid.experimental.androidTest.useUnifiedTestPlatform=false -Pandroid.testInstrumentationRunnerArguments.class=fivechan.android.MediaUploadAutomationRunnerTest",
"smoke:upload-selectors": "node scripts/smoke-upload-selectors.js",
"test:coverage": "vitest run --coverage.enabled --coverage.provider=istanbul --coverage.reporter=text --coverage.reporter=json --coverage.reporter=json-summary --coverage.reportsDirectory=./coverage",
diff --git a/scripts/write-react-doctor-badge.mjs b/scripts/write-react-doctor-badge.mjs
new file mode 100644
index 00000000..a0c50ff2
--- /dev/null
+++ b/scripts/write-react-doctor-badge.mjs
@@ -0,0 +1,44 @@
+import { execFileSync } from "node:child_process";
+import fs from "node:fs";
+import path from "node:path";
+
+const CWD = process.cwd();
+const BADGE_OUTPUT_PATH = path.join(CWD, "badges", "react-doctor.json");
+const reactDoctorArgs = ["react-doctor", ".", "--json", "--json-compact", "--yes", "--fail-on", "none"];
+
+console.log(`[react-doctor-badge] Running "yarn ${reactDoctorArgs.join(" ")}" in "${CWD}".`);
+
+const reportText = execFileSync("yarn", reactDoctorArgs, {
+ cwd: CWD,
+ encoding: "utf8",
+ stdio: ["ignore", "pipe", "inherit"],
+});
+
+let report;
+try {
+ report = JSON.parse(reportText);
+} catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ console.error(`[react-doctor-badge] Failed to parse React Doctor JSON report: ${message}`);
+ console.error(`[react-doctor-badge] Output preview: ${reportText.slice(0, 500)}`);
+ process.exit(1);
+}
+const score = report?.summary?.score;
+
+if (typeof score !== "number") {
+ console.error("[react-doctor-badge] Missing summary.score in React Doctor JSON report.");
+ process.exit(1);
+}
+
+const color = score >= 90 ? "brightgreen" : score >= 75 ? "green" : score >= 50 ? "yellow" : "red";
+const badge = {
+ schemaVersion: 1,
+ label: "react doctor",
+ message: `${score}/100`,
+ color,
+};
+
+fs.mkdirSync(path.dirname(BADGE_OUTPUT_PATH), { recursive: true });
+fs.writeFileSync(BADGE_OUTPUT_PATH, `${JSON.stringify(badge, null, 2)}\n`);
+
+console.log(`[react-doctor-badge] Wrote "${BADGE_OUTPUT_PATH}" with score ${score}/100.`);
diff --git a/src/app.tsx b/src/app.tsx
index 8cffc979..0c2049a0 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -67,22 +67,22 @@ preloadReplyModal();
const BoardLayout = () => {
const params = useParams();
const { accountCommentIndex, boardIdentifier, pageNumber } = params;
- const location = useLocation();
+ const { pathname, search } = useLocation();
const isMobile = useIsMobile();
- const isInAllView = isAllView(location.pathname);
- const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
- const isInModView = isModView(location.pathname);
+ const isInAllView = isAllView(pathname);
+ const isInSubscriptionsView = isSubscriptionsView(pathname, useParams());
+ const isInModView = isModView(pathname);
const directories = useDirectories();
const communityAddress = boardIdentifier ? getCommunityAddress(boardIdentifier, directories) : undefined;
const pendingPost = useSafeAccountComment({ commentIndex: accountCommentIndex });
const pendingPostCommunityAddress = getCommentCommunityAddress(pendingPost);
const { closeCreateBoardModal } = useCreateBoardModalStore();
- const isOnPostRoute = isPostRoute(location.pathname);
- const isOnPendingPostRoute = isPendingPostRoute(location.pathname);
- const isOnModQueueRoute = isModQueueRoute(location.pathname);
- const isOnArchiveRoute = isArchiveRoute(location.pathname);
+ const isOnPostRoute = isPostRoute(pathname);
+ const isOnPendingPostRoute = isPendingPostRoute(pathname);
+ const isOnModQueueRoute = isModQueueRoute(pathname);
+ const isOnArchiveRoute = isArchiveRoute(pathname);
const shouldRenderOutlet = isOnPostRoute || isOnPendingPostRoute || isOnModQueueRoute || isOnArchiveRoute;
- const isInCatalogView = isCatalogView(location.pathname, params);
+ const isInCatalogView = isCatalogView(pathname, params);
// Christmas theme
const { isEnabled: isSpecialEnabled } = useSpecialThemeStore();
useEffect(() => {
@@ -97,28 +97,26 @@ const BoardLayout = () => {
// Close create board modal when navigating to a different page
useEffect(() => {
closeCreateBoardModal();
- }, [location.pathname, closeCreateBoardModal]);
+ }, [pathname, closeCreateBoardModal]);
// force rerender of post form when navigating between pages, except when opening settings modal in current view
- const key = location.pathname.endsWith('/settings')
- ? `${communityAddress}-${location.pathname.replace(/\/settings$/, '')}`
- : `${communityAddress}-${location.pathname}`;
+ const key = pathname.endsWith('/settings') ? `${communityAddress}-${pathname.replace(/\/settings$/, '')}` : `${communityAddress}-${pathname}`;
if (pageNumber === '1') {
return
- Devs can change directories via commits in the open-source repo. No centralized servers—anyone can fork, modify, and redeploy to their own domain. 5chan is + Devs can change directories via commits in the open-source repo. No centralized servers; anyone can fork, modify, and redeploy to their own domain. 5chan is adminless with no central authority.