Add board directory view (#1132)

* feat(directory): add board directory view

* fix(directory): populate board status

* style(directory): tighten board table

* docs(board manager): point board owners to manager

* fix(directory): show loading status

* style(directory): center board column in directory table

* perf(directory): cap board status checks

* style(directory): simplify board row links

* test(ci): stabilize coverage run

* test(ci): stabilize coverage harness

* test(ci): avoid async app flush act

* test(app): narrow layout harness coverage

* test(ci): stabilize app update distribution mock

* test(ci): preload app harness before route tests

* fix(directory): address final review findings
This commit is contained in:
Tommaso Casaburi
2026-05-19 23:34:33 +07:00
committed by GitHub
parent 4370e89174
commit cda1f7519f
62 changed files with 1572 additions and 171 deletions
@@ -323,13 +323,14 @@ describe('BoardButtons', () => {
localStorage.clear();
});
it('renders desktop board actions for browsing boards, then searches OPs and triggers refresh, vote, subscribe, and archive flows', async () => {
it('renders desktop board actions for browsing boards, then searches OPs and triggers refresh, directory, subscribe, and archive flows', async () => {
await renderWithRoute(createElement(DesktopBoardButtons), '/mu');
expect(container.querySelector('[data-testid="mod-queue-button"]')?.textContent).toBe('mu');
expect(container.textContent).toContain('subscribe');
expect(container.textContent).toContain('vote');
expect(container.textContent).toContain('directory');
expect(findButtonLink('catalog')?.getAttribute('href')).toBe('/mu/catalog');
expect(findButtonLink('directory')?.getAttribute('href')).toBe('/mu/directory');
const searchInput = container.querySelector<HTMLInputElement>('input[type="text"]');
expect(searchInput).toBeTruthy();
@@ -345,14 +346,18 @@ describe('BoardButtons', () => {
await clickButton('refresh');
await clickButton('subscribe');
await clickButton('vote');
await clickButton('archive');
expect(testState.resetMock).toHaveBeenCalledTimes(1);
expect(testState.subscribeMock).toHaveBeenCalledTimes(1);
expect(testState.navigateMock).toHaveBeenCalledWith('/mu/archive');
expect(globalThis.alert).toHaveBeenNthCalledWith(1, 'vote_button_unavailable_intro\n\nvote_button_unavailable_outro');
expect(globalThis.alert).toHaveBeenCalledTimes(1);
expect(globalThis.alert).not.toHaveBeenCalled();
});
it('does not render the directory button on a full board address route', async () => {
await renderWithRoute(createElement(DesktopBoardButtons), '/music-posting.eth');
expect(container.textContent).not.toContain('directory');
});
it('renders desktop catalog controls and wires sort, style, filter, and refresh updates', async () => {
@@ -30,10 +30,6 @@
width: 10px;
}
.disabledButton:hover {
cursor: not-allowed !important;
}
.mobileBoardButtons a:not([class~='button']),
.desktopBoardButtons a:not([class~='button']) {
all: unset;
+12 -22
View File
@@ -7,7 +7,7 @@ import { usePostPageNumber } from '../../hooks/use-post-page-number';
import { useDirectories, useDirectoryByAddress } from '../../hooks/use-directories';
import { useAccountCommunityAddresses } from '../../hooks/use-account-community-addresses';
import { useFilteredDirectoryAddresses } from '../../hooks/use-filtered-directory-addresses';
import { getBoardPath, isDirectoryBoard } from '../../lib/utils/route-utils';
import { getBoardPath, isDirectoryRoute } from '../../lib/utils/route-utils';
import { useResolvedCommunityAddress } from '../../hooks/use-resolved-community-address';
import { useCommunityIdentifier } from '../../hooks/use-community-identifiers';
import useSafeAccountComment from '../../hooks/use-safe-account-comment';
@@ -165,26 +165,20 @@ export const ReturnButton = ({ address, isInAllView, isInSubscriptionsView, isIn
);
};
const VoteButton = () => {
const DirectoryButton = () => {
const { t } = useTranslation();
const params = useParams();
const directories = useDirectories();
// Get the boardIdentifier from params (try boardIdentifier first, then communityAddress for backward compatibility)
const boardIdentifier = params.boardIdentifier;
// Only render the vote button if we're on a directory board route
if (!boardIdentifier || !isDirectoryBoard(boardIdentifier, directories)) {
if (!boardIdentifier || !isDirectoryRoute(boardIdentifier, directories)) {
return null;
}
const values = { boardIdentifier };
const message = `${t('vote_button_unavailable_intro', values)}\n\n${t('vote_button_unavailable_outro', values)}`;
return (
<button className={`button ${styles.disabledButton}`} onClick={() => window.alert(message)}>
{t('vote')}
</button>
<Link className='button' to={`/${boardIdentifier}/directory`}>
{t('directory')}
</Link>
);
};
@@ -508,7 +502,6 @@ export const MobileAllFeedFilter = () => (
);
export const MobileBoardButtons = () => {
const { t } = useTranslation();
const params = useParams();
const location = useLocation();
const isInAllView = isAllView(location.pathname);
@@ -529,10 +522,9 @@ export const MobileBoardButtons = () => {
const effectiveInfiniteScroll = isMultiboard || enableInfiniteScroll;
const showBottomButton = !effectiveInfiniteScroll;
// Check if we should show the vote button (only for directory boards)
const directories = useDirectories();
const boardIdentifier = params.boardIdentifier;
const showVoteButton = boardIdentifier && isDirectoryBoard(boardIdentifier, directories);
const showDirectoryButton = boardIdentifier && isDirectoryRoute(boardIdentifier, directories);
return (
<div className={`${styles.mobileBoardButtons} ${!isInCatalogView ? styles.addMargin : ''}`}>
@@ -602,7 +594,7 @@ export const MobileBoardButtons = () => {
<CatalogButton address={communityAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />
<RefreshButton />
<div className={styles.secondRow}>
{showVoteButton && <VoteButton />}
{showDirectoryButton && <DirectoryButton />}
{!(isInAllView || isInSubscriptionsView || isInModView) && <SubscribeButton address={communityAddress} />}
{!(isInAllView || isInSubscriptionsView) && <ModQueueButton boardIdentifier={boardIdentifier} isMobile={true} />}
</div>
@@ -696,7 +688,6 @@ export const CatalogSearchResultsLabel = () => {
};
export const DesktopBoardButtons = () => {
const { t } = useTranslation();
const params = useParams();
const location = useLocation();
const accountComment = useSafeAccountComment({ commentIndex: params?.accountCommentIndex });
@@ -716,10 +707,9 @@ export const DesktopBoardButtons = () => {
const effectiveInfiniteScroll = isMultiboard || enableInfiniteScroll;
const showBottomButton = (isInCatalogView || isInPostView || isInPendingPostPage) && !effectiveInfiniteScroll;
// Check if we should show the vote button (only for directory boards)
const directories = useDirectories();
const boardIdentifier = params.boardIdentifier;
const showVoteButton = boardIdentifier && isDirectoryBoard(boardIdentifier, directories);
const showDirectoryButton = boardIdentifier && isDirectoryRoute(boardIdentifier, directories);
return (
<>
@@ -818,12 +808,12 @@ export const DesktopBoardButtons = () => {
{showTimeFilter && (
<TimeFilter isInAllView={isInAllView} isInCatalogView={isInCatalogView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />
)}
{showVoteButton && (
{showDirectoryButton && (
<>
[<VoteButton />]
[<DirectoryButton />]
</>
)}
{showVoteButton && !(isInAllView || isInSubscriptionsView || isInModView) && ' '}
{showDirectoryButton && !(isInAllView || isInSubscriptionsView || isInModView) && ' '}
{!(isInAllView || isInSubscriptionsView || isInModView) && (
<>
[<SubscribeButton address={communityAddress} />]
+14 -3
View File
@@ -7,7 +7,7 @@ import getShortAddress from '../../lib/get-short-address';
import { useCommunityIdentifier } from '../../hooks/use-community-identifiers';
import { useStableCommunity } from '../../hooks/use-stable-community';
import { isAllView, isSubscriptionsView, isModView } from '../../lib/utils/view-utils';
import { isArchiveRoute } from '../../lib/utils/route-utils';
import { isArchiveRoute, isDirectoryListRoute } from '../../lib/utils/route-utils';
import styles from './board-header.module.css';
import { useDirectoriesMetadata, useDirectories } from '../../hooks/use-directories';
import { useResolvedCommunityAddress } from '../../hooks/use-resolved-community-address';
@@ -54,6 +54,7 @@ const BoardHeader = () => {
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
const isInModView = isModView(location.pathname);
const isInArchiveView = isArchiveRoute(location.pathname);
const isInDirectoryListView = isDirectoryListRoute(location.pathname);
const accountComment = useSafeAccountComment({ commentIndex: params?.accountCommentIndex });
const resolvedAddress = useResolvedCommunityAddress();
const communityAddress = resolvedAddress || accountComment?.communityAddress;
@@ -83,7 +84,15 @@ const BoardHeader = () => {
: isInModView
? startCase(t('boards_you_moderate'))
: defaultCommunity?.title || stableCommunity?.title;
const subtitle = isInAllView ? '' : isInSubscriptionsView ? subscriptionsSubtitle : isInModView ? '/mod/' : `${address || communityAddress || ''}`;
const subtitle = isInAllView
? ''
: isInSubscriptionsView
? subscriptionsSubtitle
: isInModView
? '/mod/'
: isInDirectoryListView
? t('directory_subtitle', { boardIdentifier: params.boardIdentifier })
: `${address || communityAddress || ''}`;
return (
<div className={`${styles.content} ${shouldShowSnow() ? styles.garland : ''}`}>
@@ -99,7 +108,7 @@ const BoardHeader = () => {
? shortAddress.slice(0, -4)
: shortAddress
: communityAddress && getShortAddress(communityAddress))}
{!isInAllView && !isInSubscriptionsView && !isInModView && <OfflineIndicator communityAddress={communityAddress} />}
{!isInAllView && !isInSubscriptionsView && !isInModView && !isInDirectoryListView && <OfflineIndicator communityAddress={communityAddress} />}
</div>
<div className={styles.boardSubtitle}>
{isInSubscriptionsView ? (
@@ -117,6 +126,8 @@ const BoardHeader = () => {
>
{subtitle}
</span>
) : isInDirectoryListView ? (
<span>{subtitle}</span>
) : !isInAllView && !isInModView && subtitle ? (
<span title={t('board_address_tooltip')}>{subtitle}</span>
) : (
@@ -36,9 +36,9 @@ const CreateBoardModal = () => {
<div className={styles.section}>
<h3>Creating Your Board</h3>
<p>
Create a board using the CLI:
<a href='https://github.com/bitsocialnet/bitsocial-cli' target='_blank' rel='noopener noreferrer'>
bitsocial-cli
Create a board using{' '}
<a href='https://github.com/bitsocialnet/5chan-board-manager' target='_blank' rel='noopener noreferrer'>
5chan Board Manager
</a>
. <strong>Build a following:</strong> Users can subscribe to your board via the &quot;[Subscribe]&quot; button, which adds it to their top bar. You can gain
subscribers through direct links, word of mouth, or search, no directory assignment or dev approval needed.
@@ -43,9 +43,9 @@ const DirectoryModal = () => {
<div className={styles.section}>
<h3>Creating Your Board</h3>
<p>
<strong>Anyone can create a board</strong> using the official CLI:{' '}
<a href='https://github.com/bitsocialnet/bitsocial-cli' target='_blank' rel='noopener noreferrer'>
bitsocial-cli
<strong>Anyone can create a board</strong> using{' '}
<a href='https://github.com/bitsocialnet/5chan-board-manager' target='_blank' rel='noopener noreferrer'>
5chan Board Manager
</a>
. Users can access it anytime via the search bar, direct links, or by subscribing with the &quot;[Subscribe]&quot; button;{' '}
<strong>no directory assignment or dev approval needed</strong>. Directory boards are simply featured in homepage categories (like &quot;Anime &