From 556973a44571d0fe8ce6203cba7d524419b17290 Mon Sep 17 00:00:00 2001 From: Tommaso Casaburi Date: Sun, 10 May 2026 18:25:13 +0700 Subject: [PATCH] Improve React Doctor score and badge (#1127) * fix(react doctor): improve quality score and badge * fix(react doctor): address review feedback * fix(review): address final bot feedback --- .github/workflows/ci.yml | 22 +- README.md | 1 + package.json | 1 + scripts/write-react-doctor-badge.mjs | 44 ++++ src/app.tsx | 38 ++- .../board-buttons/board-buttons.tsx | 16 +- .../__tests__/board-header.test.tsx | 2 +- .../__tests__/boards-bar-edit-modal.test.tsx | 75 ++++++ .../boards-bar-edit-modal.tsx | 10 +- src/components/boards-bar/boards-bar.tsx | 30 +-- .../__tests__/catalog-filters.test.tsx | 7 +- .../catalog-filters/catalog-filters.tsx | 211 ++++++++-------- .../filters-protip/filters-protip.tsx | 30 +-- .../highlight-color-picker.module.css | 10 + .../highlight-color-picker.tsx | 9 +- .../catalog-search/catalog-search.tsx | 10 +- .../__tests__/challenge-modal.test.tsx | 2 +- .../challenge-modal/challenge-modal.tsx | 22 +- .../comment-content/comment-content.tsx | 2 +- .../create-board-modal/create-board-modal.tsx | 4 +- .../directory-modal/directory-modal.tsx | 2 +- .../markdown/__tests__/markdown.test.tsx | 38 +++ .../markdown/external-number-quote-link.tsx | 6 +- src/components/markdown/markdown.tsx | 233 +++++++++++------- .../post-menu-desktop/post-menu-desktop.tsx | 18 +- src/components/post-form/post-form.tsx | 8 +- .../post-menu-mobile/post-menu-mobile.tsx | 36 +-- .../__tests__/reply-modal.test.tsx | 44 +++- src/components/reply-modal/reply-modal.tsx | 89 ++++--- .../account-settings/account-settings.tsx | 4 +- .../advanced-settings/advanced-settings.tsx | 28 +-- .../crypto-wallets-setting.tsx | 2 +- .../p2p-stats-settings/p2p-stats-settings.tsx | 9 +- .../settings-modal/settings-modal.tsx | 10 +- .../subscriptions-setting.tsx | 6 +- src/e2e/pretext-benchmark-harness.tsx | 10 +- src/hooks/use-community-identifiers.ts | 4 +- src/hooks/use-directories.ts | 2 +- src/lib/utils/blotter-utils.ts | 2 +- src/lib/utils/pretext-height-estimates.ts | 6 +- src/lib/utils/route-utils.ts | 2 +- src/lib/utils/view-utils.ts | 2 +- src/views/archive/__tests__/helpers.ts | 2 +- src/views/archive/index.ts | 1 - .../home/boards-list/boards-filter-modal.tsx | 21 +- src/views/mod-queue/mod-queue.tsx | 63 +++-- src/views/post/post.tsx | 19 +- 47 files changed, 771 insertions(+), 442 deletions(-) create mode 100644 scripts/write-react-doctor-badge.mjs create mode 100644 src/components/boards-bar-edit-modal/__tests__/boards-bar-edit-modal.test.tsx delete mode 100644 src/views/archive/index.ts 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 @@ [![Build Status](https://img.shields.io/github/actions/workflow/status/bitsocialnet/5chan/ci.yml?branch=master)](https://github.com/bitsocialnet/5chan/actions/workflows/ci.yml) [![Coverage](https://img.shields.io/endpoint?url=https://bitsocialnet.github.io/5chan/badges/coverage.json)](https://github.com/bitsocialnet/5chan/blob/master/scripts/write-coverage-badge.mjs) +[![React Doctor](https://img.shields.io/endpoint?url=https://bitsocialnet.github.io/5chan/badges/react-doctor.json)](https://github.com/bitsocialnet/5chan/actions/workflows/ci.yml) [![Release](https://img.shields.io/github/v/release/bitsocialnet/5chan)](https://github.com/bitsocialnet/5chan/releases/latest) [![License](https://img.shields.io/badge/license-GPL--3.0--or--later-red.svg)](https://github.com/bitsocialnet/5chan/blob/master/LICENSE) [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](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 ; } // Invalid /mod/ paths (e.g. /mod/modqueue, /mod/asdoijasd) -> not-found - if (location.pathname.startsWith('/mod/') && !isValidModRoute(location.pathname)) { + if (pathname.startsWith('/mod/') && !isValidModRoute(pathname)) { return ; } - if (isLegacyBoardModQueueRoute(location.pathname)) { + if (isLegacyBoardModQueueRoute(pathname)) { return ; } // Invalid board-scoped mod paths (e.g. /biz/mod, /biz/mod/asdoijasd) -> not-found - if (isBoardModRoute(location.pathname) && !isValidBoardModRoute(location.pathname)) { + if (isBoardModRoute(pathname) && !isValidBoardModRoute(pathname)) { return ; } @@ -126,8 +124,8 @@ const BoardLayout = () => { if (boardIdentifier && !isDirectoryBoard(boardIdentifier, directories)) { const canonicalBoardIdentifier = getBoardPath(boardIdentifier, directories); if (canonicalBoardIdentifier !== boardIdentifier) { - const canonicalPath = location.pathname.replace(`/${boardIdentifier}`, `/${canonicalBoardIdentifier}`); - return ; + const canonicalPath = pathname.replace(`/${boardIdentifier}`, `/${canonicalBoardIdentifier}`); + return ; } } @@ -201,8 +199,8 @@ const GlobalLayout = () => { })), ); - const location = useLocation(); - const isInSettingsView = location.pathname.endsWith('/settings'); + const { pathname } = useLocation(); + const isInSettingsView = pathname.endsWith('/settings'); return ( <> diff --git a/src/components/board-buttons/board-buttons.tsx b/src/components/board-buttons/board-buttons.tsx index 9573d92a..a107b335 100644 --- a/src/components/board-buttons/board-buttons.tsx +++ b/src/components/board-buttons/board-buttons.tsx @@ -221,11 +221,11 @@ export const AutoButton = () => { export const BottomButton = () => { const { t } = useTranslation(); - const handleClick = () => { + const scrollToBottom = () => { window.scrollTo({ top: document.documentElement.scrollHeight, behavior: 'instant' }); }; return ( - ); @@ -233,11 +233,11 @@ export const BottomButton = () => { export const TopButton = () => { const { t } = useTranslation(); - const handleClick = () => { + const scrollToTop = () => { window.scrollTo({ top: 0, left: 0, behavior: 'instant' }); }; return ( - ); @@ -507,13 +507,13 @@ export const MobileBoardButtons = () => { {searchText ? ( {' '} - — {t('search_results_for')}: {searchText} + - {t('search_results_for')}: {searchText} ) : ( filteredCount > 0 && ( {' '} - — {t('filtered_threads')}: {filteredCount} + - {t('filtered_threads')}: {filteredCount} ) )} @@ -714,14 +714,14 @@ export const DesktopBoardButtons = () => { {isInCatalogView && searchText ? ( {' '} - — {t('search_results_for')}: {searchText} + - {t('search_results_for')}: {searchText} ) : ( isInCatalogView && filteredCount > 0 && ( {' '} - — {t('filtered_threads')}: {filteredCount} + - {t('filtered_threads')}: {filteredCount} ) )} diff --git a/src/components/board-header/__tests__/board-header.test.tsx b/src/components/board-header/__tests__/board-header.test.tsx index f96f410f..1c0febed 100644 --- a/src/components/board-header/__tests__/board-header.test.tsx +++ b/src/components/board-header/__tests__/board-header.test.tsx @@ -62,7 +62,7 @@ vi.mock('@bitsocial/bitsocial-react-hooks/dist/stores/accounts', () => ({ selector({ accounts: { active: { - subscriptions: new Array(testState.subscriptionsCount).fill('sub'), + subscriptions: Array.from({ length: testState.subscriptionsCount }, () => 'sub'), }, }, activeAccountId: 'active', diff --git a/src/components/boards-bar-edit-modal/__tests__/boards-bar-edit-modal.test.tsx b/src/components/boards-bar-edit-modal/__tests__/boards-bar-edit-modal.test.tsx new file mode 100644 index 00000000..746cbff0 --- /dev/null +++ b/src/components/boards-bar-edit-modal/__tests__/boards-bar-edit-modal.test.tsx @@ -0,0 +1,75 @@ +import * as React from 'react'; +import { createElement } from 'react'; +import { createRoot, type Root } from 'react-dom/client'; +import { MemoryRouter } from 'react-router-dom'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import BoardsBarEditModal from '../boards-bar-edit-modal'; +import useBoardsBarEditModalStore from '../../../stores/use-boards-bar-edit-modal-store'; +import useBoardsBarVisibilityStore from '../../../stores/use-boards-bar-visibility-store'; + +(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; +const act = (React as { act?: (cb: () => void | Promise) => void | Promise }).act as (cb: () => void | Promise) => void | Promise; + +vi.mock('@bitsocial/bitsocial-react-hooks', () => ({ + useAccount: () => ({ + subscriptions: ['custom.eth'], + }), +})); + +let container: HTMLDivElement; +let root: Root; + +const renderModal = async () => { + await act(async () => { + root.render(createElement(MemoryRouter, { initialEntries: ['/tv/catalog'] }, createElement(BoardsBarEditModal))); + }); +}; + +describe('BoardsBarEditModal', () => { + beforeEach(() => { + localStorage.clear(); + useBoardsBarEditModalStore.setState({ showModal: true }); + useBoardsBarVisibilityStore.setState({ + visibleDirectories: new Set(['tv']), + showSubscriptionsInBoardsBar: false, + }); + + container = document.createElement('div'); + document.body.appendChild(container); + root = createRoot(container); + }); + + afterEach(() => { + act(() => root.unmount()); + container.remove(); + useBoardsBarEditModalStore.setState({ showModal: false }); + localStorage.clear(); + }); + + it('keeps the modal open when typing spaces in the directory input', async () => { + await renderModal(); + + const input = container.querySelector('input[aria-label="Directory codes"]'); + expect(input).toBeTruthy(); + + await act(async () => { + input?.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true })); + }); + + expect(container.querySelector('[role="dialog"]')).toBeTruthy(); + expect(useBoardsBarEditModalStore.getState().showModal).toBe(true); + }); + + it('still closes when the backdrop itself handles keyboard activation', async () => { + await renderModal(); + + const backdrop = container.querySelector('[role="button"]'); + expect(backdrop).toBeTruthy(); + + await act(async () => { + backdrop?.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true })); + }); + + expect(useBoardsBarEditModalStore.getState().showModal).toBe(false); + }); +}); diff --git a/src/components/boards-bar-edit-modal/boards-bar-edit-modal.tsx b/src/components/boards-bar-edit-modal/boards-bar-edit-modal.tsx index 4ad8bbf2..c5f84278 100644 --- a/src/components/boards-bar-edit-modal/boards-bar-edit-modal.tsx +++ b/src/components/boards-bar-edit-modal/boards-bar-edit-modal.tsx @@ -12,8 +12,12 @@ const stringToDirectories = (str: string): Set => { const codes = str .trim() .split(/\s+/) - .filter((code) => code.length > 0) - .map((code) => code.toLowerCase()); + .reduce((items, code) => { + if (code.length > 0) { + items.push(code.toLowerCase()); + } + return items; + }, []); return new Set(codes); }; @@ -118,7 +122,7 @@ const BoardsBarEditModal = () => { role='button' tabIndex={0} onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { + if (e.target === e.currentTarget && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); closeBoardsBarEditModal(); } diff --git a/src/components/boards-bar/boards-bar.tsx b/src/components/boards-bar/boards-bar.tsx index c60cff42..a0b9c11d 100644 --- a/src/components/boards-bar/boards-bar.tsx +++ b/src/components/boards-bar/boards-bar.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import getShortAddress from '../../lib/get-short-address'; @@ -32,21 +32,18 @@ const SearchBar = ({ setShowSearchBar }: { setShowSearchBar: (show: boolean) => searchInputRef.current?.focus(); }, []); - const handleClickOutside = useCallback( - (event: MouseEvent) => { + useEffect(() => { + const closeSearchOnOutsideClick = (event: MouseEvent) => { if (searchBarRef.current && !searchBarRef.current.contains(event.target as Node)) { setShowSearchBar(false); } - }, - [searchBarRef, setShowSearchBar], - ); - - useEffect(() => { - document.addEventListener('mousedown', handleClickOutside); - return () => { - document.removeEventListener('mousedown', handleClickOutside); }; - }, [handleClickOutside]); + + document.addEventListener('mousedown', closeSearchOnOutsideClick); + return () => { + document.removeEventListener('mousedown', closeSearchOnOutsideClick); + }; + }, [setShowSearchBar]); useEffect(() => { const handleEscapeKey = (event: KeyboardEvent) => { @@ -113,8 +110,7 @@ const BoardsBarDesktop = () => { return [...(activeAccount?.subscriptions || [])]; }, (prev, next) => { - if (prev.length !== next.length) return false; - return prev.every((val, idx) => val === next[idx]); + return prev.length === next.length && prev.every((val, idx) => val === next[idx]); }, ); @@ -146,7 +142,7 @@ const BoardsBarDesktop = () => { const address = findBoardAddressByCode(code, directories); const isPlaceholder = !address; - const handleClick = (e: React.MouseEvent) => { + const openDirectoryForPlaceholder = (e: React.MouseEvent) => { // If no address exists, prevent navigation and open directory modal if (!address) { e.preventDefault(); @@ -168,13 +164,13 @@ const BoardsBarDesktop = () => { if (!address) openDirectoryModal(); } }} - onClick={handleClick} + onClick={openDirectoryForPlaceholder} style={{ cursor: 'pointer' }} > {code} ) : ( - + {code} )} diff --git a/src/components/catalog-filters/__tests__/catalog-filters.test.tsx b/src/components/catalog-filters/__tests__/catalog-filters.test.tsx index 2fecc7b8..805a2c26 100644 --- a/src/components/catalog-filters/__tests__/catalog-filters.test.tsx +++ b/src/components/catalog-filters/__tests__/catalog-filters.test.tsx @@ -212,7 +212,7 @@ describe('CatalogFilters', () => { expect(addedRowInputs[3]?.checked).toBe(false); }); - it('reorders, edits, and saves non-empty filters via the document Enter shortcut', async () => { + it('reorders, edits, and saves non-empty filters from the form', async () => { renderCatalogFilters(); await openModal(); @@ -255,6 +255,11 @@ describe('CatalogFilters', () => { await act(async () => { document.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: 'Enter' })); }); + expect(testState.saveAndApplyFiltersMock).not.toHaveBeenCalled(); + + await act(async () => { + container.querySelector('form')?.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); + }); expect(testState.saveAndApplyFiltersMock).toHaveBeenCalledTimes(1); const savedFilters = testState.saveAndApplyFiltersMock.mock.calls[0]?.[0] as FilterItem[] | undefined; diff --git a/src/components/catalog-filters/catalog-filters.tsx b/src/components/catalog-filters/catalog-filters.tsx index 1dbb9f32..2dbab9a3 100644 --- a/src/components/catalog-filters/catalog-filters.tsx +++ b/src/components/catalog-filters/catalog-filters.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useRef, useEffect } from 'react'; +import { useState, useCallback, useRef, useEffect, type FormEvent } from 'react'; import { useTranslation } from 'react-i18next'; import { useShallow } from 'zustand/react/shallow'; import useCatalogFiltersStore from '../../stores/use-catalog-filters-store'; @@ -91,7 +91,12 @@ const FiltersTable = ({ onSave }: { onSave: () => void }) => { }, []); const handleSave = useCallback(() => { - const nonEmptyFilters = localFilterItems.filter((item) => item.text.trim() !== '').map(({ id: _id, ...rest }) => rest); + const nonEmptyFilters = localFilterItems.reduce[]>((filters, item) => { + if (item.text.trim() === '') return filters; + const { id: _id, ...rest } = item; + filters.push(rest); + return filters; + }, []); saveAndApplyFilters(nonEmptyFilters); @@ -104,20 +109,14 @@ const FiltersTable = ({ onSave }: { onSave: () => void }) => { onSave(); }, [saveAndApplyFilters, localFilterItems, onSave, resetFeed]); - const handleKeyDown = useCallback( - (e: KeyboardEvent) => { - if (e.key === 'Enter') { - handleSave(); - } + const handleSubmit = useCallback( + (event: FormEvent) => { + event.preventDefault(); + handleSave(); }, [handleSave], ); - useEffect(() => { - document.addEventListener('keydown', handleKeyDown); - return () => document.removeEventListener('keydown', handleKeyDown); - }, [handleKeyDown]); - const updateLocalFilterItem = useCallback((index: number, item: any) => { setLocalFilterItems((prev) => prev.map((f, i) => (i === index ? item : f))); }, []); @@ -136,100 +135,104 @@ const FiltersTable = ({ onSave }: { onSave: () => void }) => { }, []); return ( - - - - - - - - - - - - - - {localFilterItems.map((item, index) => ( - - - - - - - - -
orderonpatterncolorhidetopdel
- { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - moveLocalFilterItemUp(index); - } - }} - onClick={() => moveLocalFilterItemUp(index)} - > - ↑ - - - updateLocalFilterItem(index, { ...item, enabled: e.target.checked })} - /> - - updateLocalFilterItem(index, { ...item, text: e.target.value })} - ref={(el) => (inputRefs.current[index] = el)} - /> - - - - updateLocalFilterItem(index, { ...item, hide: e.target.checked })} /> - - updateLocalFilterItem(index, { ...item, top: e.target.checked })} /> - - { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - removeLocalFilterItem(index); - } - }} - onClick={() => removeLocalFilterItem(index)} - > - × - - - {currentCommunityAddress && item.communityFilteredCids?.has(currentCommunityAddress) && `x${item.communityCounts?.get(currentCommunityAddress) ?? 0}`} +
+ + + + + + + + + + + + + + {localFilterItems.map((item, index) => ( + + + + + + + + + + + ))} + + + + - ))} - - - - - - -
orderonpatterncolorhidetopdel
+ { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + moveLocalFilterItemUp(index); + } + }} + onClick={() => moveLocalFilterItemUp(index)} + > + ↑ + + + updateLocalFilterItem(index, { ...item, enabled: e.target.checked })} + /> + + updateLocalFilterItem(index, { ...item, text: e.target.value })} + ref={(el) => { + inputRefs.current[index] = el; + }} + /> + + + + updateLocalFilterItem(index, { ...item, hide: e.target.checked })} /> + + updateLocalFilterItem(index, { ...item, top: e.target.checked })} /> + + { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + removeLocalFilterItem(index); + } + }} + onClick={() => removeLocalFilterItem(index)} + > + × + + + {currentCommunityAddress && item.communityFilteredCids?.has(currentCommunityAddress) && `x${item.communityCounts?.get(currentCommunityAddress) ?? 0}`} +
+ +
- - -
+ +
+ ); }; diff --git a/src/components/catalog-filters/filters-protip/filters-protip.tsx b/src/components/catalog-filters/filters-protip/filters-protip.tsx index 5ae12370..afe00654 100644 --- a/src/components/catalog-filters/filters-protip/filters-protip.tsx +++ b/src/components/catalog-filters/filters-protip/filters-protip.tsx @@ -9,7 +9,7 @@ const FiltersProtip = () => { Matching whole words:
  • - feel — will match "feel" but not "feeling". This search is case-insensitive. + feel: will match "feel" but not "feeling". This search is case-insensitive.
    • @@ -17,7 +17,7 @@ const FiltersProtip = () => { AND operator:
    • - feel girlfriend — will match "feel" AND "girlfriend" in any order. + feel girlfriend: will match "feel" AND "girlfriend" in any order.
      @@ -25,7 +25,7 @@ const FiltersProtip = () => { OR operator:
    • - feel|girlfriend — will match "feel" OR "girlfriend". + feel|girlfriend: will match "feel" OR "girlfriend".
      @@ -33,7 +33,7 @@ const FiltersProtip = () => { Mixing both operators:
    • - girlfriend|boyfriend feel — matches "feel" AND "girlfriend", or "feel" AND "boyfriend". + girlfriend|boyfriend feel: matches "feel" AND "girlfriend", or "feel" AND "boyfriend".
      @@ -41,7 +41,7 @@ const FiltersProtip = () => { Exact match search:
    • - "that feel when" — place double quotes around the pattern to search for an exact string. + "that feel when": place double quotes around the pattern to search for an exact string.
      @@ -49,10 +49,10 @@ const FiltersProtip = () => { Wildcards:
    • - feel* — matches expressions such as "feel", "feels", "feeling", "feeler", etc… + feel*: matches expressions such as "feel", "feels", "feeling", "feeler", etc…
    • - idolm*ster — this can match "idolmaster" or "idolm@ster", etc… + idolm*ster: this can match "idolmaster" or "idolm@ster", etc…
      @@ -70,31 +70,31 @@ const FiltersProtip = () => {
        It is also possible to filter by regular expression:
      • - /^(?=.*detachable)(?=.*hats).*$/i — AND operator. + /^(?=.*detachable)(?=.*hats).*$/i: AND operator.
      • - /^(?!.*touhou).*$/i — NOT operator. + /^(?!.*touhou).*$/i: NOT operator.
      • - {'/^>/'} — threads starting with a quote ({'">"'} character as an html entity). + {'/^>/'}: threads starting with a quote ({'">"'} character as an html entity).
      • - /^$/ — threads with no text. + /^$/: threads with no text.

      Controls

      • - On — enables or disables the filter. + On: enables or disables the filter.
      • - Color — highlights matched threads with the specified color. + Color: highlights matched threads with the specified color.
      • - Hide — hides matched threads. + Hide: hides matched threads.
      • - Top — moves the filter to the top of the feed. + Top: moves the filter to the top of the feed.
      diff --git a/src/components/catalog-filters/highlight-color-picker/highlight-color-picker.module.css b/src/components/catalog-filters/highlight-color-picker/highlight-color-picker.module.css index 38bdf5c1..04f79cac 100644 --- a/src/components/catalog-filters/highlight-color-picker/highlight-color-picker.module.css +++ b/src/components/catalog-filters/highlight-color-picker/highlight-color-picker.module.css @@ -51,6 +51,16 @@ cursor: pointer; } +.colorPreview { + display: inline-block; + width: 16px; + height: 16px; + border: 1px solid #aaa; + vertical-align: middle; + margin-left: 5px; + cursor: pointer; +} + .middleTxt input[type="text"] { width: 45px; margin: 0 2px; diff --git a/src/components/catalog-filters/highlight-color-picker/highlight-color-picker.tsx b/src/components/catalog-filters/highlight-color-picker/highlight-color-picker.tsx index 75f93e69..2a5cf0f6 100644 --- a/src/components/catalog-filters/highlight-color-picker/highlight-color-picker.tsx +++ b/src/components/catalog-filters/highlight-color-picker/highlight-color-picker.tsx @@ -83,7 +83,7 @@ const HighlightColorPicker = ({ item, index, updateLocalFilterItem, localFilterI left: 0, right: 0, bottom: 0, - zIndex: 999, + zIndex: 30, }} role='button' tabIndex={0} @@ -144,13 +144,6 @@ const HighlightColorPicker = ({ item, index, updateLocalFilterItem, localFilterI className={styles.colorPreview} style={{ backgroundColor: customColor || '#fff', - display: 'inline-block', - width: '16px', - height: '16px', - border: '1px solid #aaa', - verticalAlign: 'middle', - marginLeft: '5px', - cursor: 'pointer', }} role='button' tabIndex={0} diff --git a/src/components/catalog-search/catalog-search.tsx b/src/components/catalog-search/catalog-search.tsx index 25ccd884..af67d03b 100644 --- a/src/components/catalog-search/catalog-search.tsx +++ b/src/components/catalog-search/catalog-search.tsx @@ -8,11 +8,11 @@ import debounce from 'lodash/debounce'; const CatalogSearch = () => { const { t } = useTranslation(); - const location = useLocation(); + const { pathname, search } = useLocation(); const navigate = useNavigate(); const [searchState, setSearchState] = useState({ open: false, value: '' }); const { setSearchFilter, clearSearchFilter } = useCatalogFiltersStore(); - const queryParam = new URLSearchParams(location.search).get('q') ?? ''; + const queryParam = new URLSearchParams(search).get('q') ?? ''; const openSearch = !!queryParam || searchState.open; const inputValue = searchState.open || searchState.value ? searchState.value : queryParam; @@ -27,17 +27,17 @@ const CatalogSearch = () => { const updateURL = useCallback( (searchText: string) => { - const urlParams = new URLSearchParams(location.search); + const urlParams = new URLSearchParams(search); if (searchText.trim()) { urlParams.set('q', searchText); } else { urlParams.delete('q'); } const newSearch = urlParams.toString(); - const newPath = location.pathname + (newSearch ? `?${newSearch}` : ''); + const newPath = pathname + (newSearch ? `?${newSearch}` : ''); navigate(newPath, { replace: true }); }, - [location.pathname, location.search, navigate], + [pathname, search, navigate], ); const debouncedSetSearchFilter = useMemo( diff --git a/src/components/challenge-modal/__tests__/challenge-modal.test.tsx b/src/components/challenge-modal/__tests__/challenge-modal.test.tsx index d846348a..e0e5fd4e 100644 --- a/src/components/challenge-modal/__tests__/challenge-modal.test.tsx +++ b/src/components/challenge-modal/__tests__/challenge-modal.test.tsx @@ -318,7 +318,7 @@ describe('ChallengeModal', () => { 'https://mintpass.org', ); - await clickButton('Done'); + await clickButton('Close challenge'); expect(publication.publishChallengeAnswers).toHaveBeenCalledWith(['']); expect(testState.removeChallengeMock).toHaveBeenCalledOnce(); }); diff --git a/src/components/challenge-modal/challenge-modal.tsx b/src/components/challenge-modal/challenge-modal.tsx index d85a060d..03bc9390 100644 --- a/src/components/challenge-modal/challenge-modal.tsx +++ b/src/components/challenge-modal/challenge-modal.tsx @@ -129,6 +129,8 @@ const IframeChallenge = ({ const attemptedLoadRef = useRef(false); const mountedRef = useRef(false); const handledAutoCompleteRef = useRef(false); + const onAutoCompleteRef = useRef(onAutoComplete); + onAutoCompleteRef.current = onAutoComplete; const expectedSessionId = getIframeSessionId(challenge); useEffect(() => { @@ -239,12 +241,12 @@ const IframeChallenge = ({ const sessionId = (data as { sessionId?: unknown }).sessionId; if (sessionId !== expectedSessionId) return; handledAutoCompleteRef.current = true; - onAutoComplete(challengeAnswers.filter((answer): answer is string => typeof answer === 'string')); + onAutoCompleteRef.current(challengeAnswers.filter((answer): answer is string => typeof answer === 'string')); }; window.addEventListener('message', handleMessage); return () => window.removeEventListener('message', handleMessage); - }, [expectedSessionId, iframeOrigin, onAutoComplete]); + }, [expectedSessionId, iframeOrigin]); if (!iframeUrlState) { return ( @@ -289,7 +291,7 @@ const IframeChallenge = ({
      - +
      @@ -339,11 +341,9 @@ const Challenge = ({ challenge, closeModal, abandonModal }: ChallengeProps) => { ({ active, event, offset: [ox, oy] }) => { if (active) { event.preventDefault(); - document.body.style.userSelect = 'none'; - document.body.style.webkitUserSelect = 'none'; + Object.assign(document.body.style, { userSelect: 'none', webkitUserSelect: 'none' }); } else { - document.body.style.userSelect = ''; - document.body.style.webkitUserSelect = ''; + Object.assign(document.body.style, { userSelect: '', webkitUserSelect: '' }); } api.start({ x: ox, y: oy, immediate: true }); }, @@ -449,21 +449,21 @@ const Challenge = ({ challenge, closeModal, abandonModal }: ChallengeProps) => { const publicationDetails = ( <>
      - +
      {title && (
      - +
      )} {content && (
      -