feat(home): redesign boards list to match 4chan with filtering and catalog support

This commit is contained in:
plebeius
2025-11-05 19:50:31 +01:00
parent e40eb5312b
commit a9690ac0be
7 changed files with 464 additions and 173 deletions
+239 -66
View File
@@ -1,100 +1,273 @@
import { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import Plebbit from '@plebbit/plebbit-js';
import { useDefaultSubplebbitsState, MultisubSubplebbit } from '../../../hooks/use-default-subplebbits';
import useIsMobile from '../../../hooks/use-is-mobile';
import LoadingEllipsis from '../../../components/loading-ellipsis';
import useDisclaimerModalStore from '../../../stores/use-disclaimer-modal-store';
import styles from './boards-list.module.css';
import useBoardsFilterStore from '../../../stores/use-boards-filter-store';
import BoardsFilterModal from './boards-filter-modal';
import styles from '../home.module.css';
const Board = ({ subplebbit, isMobile }: { subplebbit: MultisubSubplebbit; isMobile: boolean }) => {
const { t } = useTranslation();
const { address, title, nsfw } = subplebbit || {};
const boardTitle = title?.replace(/^\/[^/]+\/\s*-\s*/, '') || '';
const displayAddress = address && Plebbit.getShortAddress(address);
const navigate = useNavigate();
const { showDisclaimerModal } = useDisclaimerModalStore();
const handleLinkClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
if (address) {
showDisclaimerModal(address, navigate);
}
};
// Helper function to find board address by matching title pattern
const findBoardAddress = (multisub: MultisubSubplebbit[], titlePattern: string): string | null => {
const entry = multisub.find((ms) => ms.title === titlePattern);
return entry?.address || null;
};
const NSFWBadge = () => {
return (
<tr key={address}>
<td>
<p className={styles.boardCell}>
<Link to={`/p/${address}`} onClick={handleLinkClick}>
{displayAddress}
</Link>
{nsfw && <span className={styles.nsfw}> ({t('nsfw')})</span>}
</p>
</td>
<td>
<p className={styles.boardCell}>{boardTitle || displayAddress}</p>
</td>
</tr>
<>
&nbsp;
<h3 className={styles.nsfwBadge}>
<span title='Not Safe For Work'>
<sup>(NSFW)</sup>
</span>
</h3>
</>
);
};
const BoardsList = ({ multisub }: { multisub: MultisubSubplebbit[] }) => {
const { t } = useTranslation();
const [displayCount, setDisplayCount] = useState(15);
const isMobile = useIsMobile();
const navigate = useNavigate();
const { loading, error } = useDefaultSubplebbitsState();
const { showDisclaimerModal } = useDisclaimerModalStore();
const { useCatalogLinks, boardFilter } = useBoardsFilterStore();
const handleLinkClick = (e: React.MouseEvent<HTMLAnchorElement>, address: string) => {
e.preventDefault();
showDisclaimerModal(address, navigate);
};
// Helper to generate link URL with optional catalog suffix
const getBoardLink = (address: string | null): string => {
if (!address) return '#';
return `/p/${address}${useCatalogLinks ? '/catalog' : ''}`;
};
if (loading) {
return (
<div className={styles.boardsBox}>
<span className={styles.loading}>
<div className={styles.box}>
<div className={`${styles.boxBar} ${styles.color2ColorBar}`}>
<h2 className='capitalize'>{t('boards')}</h2>
<BoardsFilterModal />
</div>
<div className={styles.boxContent}>
<LoadingEllipsis string={t('loading_default_boards')} />
</span>
</div>
</div>
);
}
if (error) {
return (
<div className={styles.boardsBox}>
<div className='red'>{error.message}</div>
<div className={styles.box}>
<div className={`${styles.boxBar} ${styles.color2ColorBar}`}>
<h2 className='capitalize'>{t('boards')}</h2>
<BoardsFilterModal />
</div>
<div className={styles.boxContent}>
<div className='red'>{error.message}</div>
</div>
</div>
);
}
const filteredBoards = multisub.slice(0, displayCount);
const totalBoardCount = multisub.length;
const hasMoreBoards = displayCount < multisub.length;
// Find active boards
const bizAddress = findBoardAddress(multisub, '/biz/ - Business & Finance');
const polAddress = findBoardAddress(multisub, '/pol/ - Politically Incorrect');
// Filtering logic: determine which categories to show
const showAll = boardFilter === 'all';
const showNsfwOnly = boardFilter === 'nsfw';
const showWorksafeOnly = boardFilter === 'worksafe';
// Category visibility based on filter
const showJapaneseCulture = showAll || showWorksafeOnly;
const showVideoGames = showAll || showWorksafeOnly;
const showInterests = showAll || showWorksafeOnly;
const showCreative = showAll || showWorksafeOnly;
const showOther = showAll || showWorksafeOnly;
const showMisc = showAll || showNsfwOnly;
const showAdult = showAll || showNsfwOnly;
return (
<div className={styles.boardsBox}>
<table className={styles.boardsList}>
<colgroup>
<col className={styles.boardAddress} />
<col className={styles.boardTitle} />
</colgroup>
<thead>
<tr>
<th>{t('board')}</th>
<th>{t('title')}</th>
</tr>
</thead>
<tbody>
{filteredBoards.map((sub) => (
<Board key={sub.address} subplebbit={sub} isMobile={isMobile} />
))}
</tbody>
</table>
<div className={styles.displayCount}>
{t('displaying_boards', { filteredBoardsCount: filteredBoards.length, totalBoardCount: totalBoardCount, interpolation: { escapeValue: false } })}
<div className={styles.box}>
<div className={`${styles.boxBar} ${styles.color2ColorBar}`}>
<h2 className='capitalize'>{t('boards')}</h2>
<BoardsFilterModal />
</div>
<div className={`${styles.boxContent} ${styles.boardsContent}`}>
{/* Japanese Culture */}
{showJapaneseCulture && (
<div className={styles.boardsColumn}>
<h3>Japanese Culture</h3>
<ul>
{/* Placeholder boards */}
<li>
<Link to='#' onClick={(e) => e.preventDefault()}>
Anime & Manga
</Link>
</li>
<li>
<Link to='#' onClick={(e) => e.preventDefault()}>
Anime/Cute
</Link>
</li>
<li>
<Link to='#' onClick={(e) => e.preventDefault()}>
Anime/Wallpapers
</Link>
</li>
</ul>
</div>
)}
{/* Video Games */}
{showVideoGames && (
<div className={styles.boardsColumn}>
<h3>Video Games</h3>
<ul>
{/* Placeholder boards */}
<li>
<Link to='#' onClick={(e) => e.preventDefault()}>
Video Games
</Link>
</li>
<li>
<Link to='#' onClick={(e) => e.preventDefault()}>
Video Game Generals
</Link>
</li>
</ul>
</div>
)}
{/* Interests */}
{showInterests && (
<div className={styles.boardsColumn}>
<h3>Interests</h3>
<ul>
{/* Placeholder boards */}
<li>
<Link to='#' onClick={(e) => e.preventDefault()}>
Technology
</Link>
</li>
<li>
<Link to='#' onClick={(e) => e.preventDefault()}>
Comics & Cartoons
</Link>
</li>
<li>
<Link to='#' onClick={(e) => e.preventDefault()}>
Television & Film
</Link>
</li>
</ul>
</div>
)}
{/* Creative */}
{showCreative && (
<div className={styles.boardsColumn}>
<h3>Creative</h3>
<ul>
{/* Placeholder boards */}
<li>
<Link to='#' onClick={(e) => e.preventDefault()}>
Photography
</Link>
</li>
<li>
<Link to='#' onClick={(e) => e.preventDefault()}>
Literature
</Link>
</li>
<li>
<Link to='#' onClick={(e) => e.preventDefault()}>
Music
</Link>
</li>
</ul>
</div>
)}
{/* Other - ACTIVE: biz */}
{showOther && (
<div className={styles.boardsColumn}>
<h3>Other</h3>
<ul>
{bizAddress && (
<li>
<Link to={getBoardLink(bizAddress)} onClick={(e) => handleLinkClick(e, bizAddress)}>
Business & Finance
</Link>
</li>
)}
{/* Placeholder boards */}
<li>
<Link to='#' onClick={(e) => e.preventDefault()}>
Travel
</Link>
</li>
<li>
<Link to='#' onClick={(e) => e.preventDefault()}>
Fitness
</Link>
</li>
</ul>
</div>
)}
{/* Misc. (NSFW) - ACTIVE: pol */}
{showMisc && (
<div className={styles.boardsColumn}>
<h3>Misc.</h3>
<NSFWBadge />
<ul>
{/* Placeholder boards */}
<li>
<Link to='#' onClick={(e) => e.preventDefault()}>
Random
</Link>
</li>
<li>
<Link to='#' onClick={(e) => e.preventDefault()}>
ROBOT9001
</Link>
</li>
{polAddress && (
<li>
<Link to={getBoardLink(polAddress)} onClick={(e) => handleLinkClick(e, polAddress)}>
Politically Incorrect
</Link>
</li>
)}
</ul>
</div>
)}
{/* Adult (NSFW) */}
{showAdult && (
<div className={styles.boardsColumn}>
<h3>Adult</h3>
<NSFWBadge />
<ul>
{/* Placeholder boards */}
<li>
<Link to='#' onClick={(e) => e.preventDefault()}>
Sexy Beautiful Women
</Link>
</li>
<li>
<Link to='#' onClick={(e) => e.preventDefault()}>
Hardcore
</Link>
</li>
</ul>
</div>
)}
</div>
{hasMoreBoards && (
<div className={styles.loadMoreButton}>
[<span onClick={() => setDisplayCount((prevCount) => prevCount + 15)}>{t('load_more')}</span>]
</div>
)}
</div>
);
};