mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
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:
@@ -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;
|
||||
|
||||
@@ -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} />]
|
||||
|
||||
@@ -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 "[Subscribe]" 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 "[Subscribe]" button;{' '}
|
||||
<strong>no directory assignment or dev approval needed</strong>. Directory boards are simply featured in homepage categories (like "Anime &
|
||||
|
||||
Reference in New Issue
Block a user