mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
feat(home): redesign boards list to match 4chan with filtering and catalog support
This commit is contained in:
@@ -190,7 +190,7 @@
|
|||||||
"show_all_boards": "Show All Boards",
|
"show_all_boards": "Show All Boards",
|
||||||
"show_nsfw_content_only": "Show NSFW Content Only",
|
"show_nsfw_content_only": "Show NSFW Content Only",
|
||||||
"show_nsfw_boards_only": "Show NSFW Boards Only",
|
"show_nsfw_boards_only": "Show NSFW Boards Only",
|
||||||
"use_catalog": "use catalog",
|
"use_catalog": "Use Catalog",
|
||||||
"show_worksafe_boards_only": "Show Worksafe Boards Only",
|
"show_worksafe_boards_only": "Show Worksafe Boards Only",
|
||||||
"show_all_content": "Show All Content",
|
"show_all_content": "Show All Content",
|
||||||
"link_type": "Link type",
|
"link_type": "Link type",
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
export type BoardFilter = 'all' | 'nsfw' | 'worksafe';
|
||||||
|
|
||||||
|
interface BoardsFilterStore {
|
||||||
|
useCatalogLinks: boolean;
|
||||||
|
setUseCatalogLinks: (value: boolean) => void;
|
||||||
|
boardFilter: BoardFilter;
|
||||||
|
setBoardFilter: (value: BoardFilter) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStoredUseCatalogLinks = (): boolean => {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem('5chan-boards-use-catalog');
|
||||||
|
return stored === 'true';
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStoredBoardFilter = (): BoardFilter => {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem('5chan-boards-filter') as BoardFilter;
|
||||||
|
return stored === 'nsfw' || stored === 'worksafe' ? stored : 'all';
|
||||||
|
} catch {
|
||||||
|
return 'all';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const useBoardsFilterStore = create<BoardsFilterStore>((set) => ({
|
||||||
|
useCatalogLinks: getStoredUseCatalogLinks(),
|
||||||
|
setUseCatalogLinks: (value: boolean) => {
|
||||||
|
set({ useCatalogLinks: value });
|
||||||
|
try {
|
||||||
|
localStorage.setItem('5chan-boards-use-catalog', value.toString());
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to save useCatalogLinks to localStorage:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
boardFilter: getStoredBoardFilter(),
|
||||||
|
setBoardFilter: (value: BoardFilter) => {
|
||||||
|
set({ boardFilter: value });
|
||||||
|
try {
|
||||||
|
localStorage.setItem('5chan-boards-filter', value);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to save boardFilter to localStorage:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default useBoardsFilterStore;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { NavigateFunction } from 'react-router-dom';
|
import { NavigateFunction } from 'react-router-dom';
|
||||||
|
|
||||||
const DISCLAIMER_ACCEPTED_KEY = '5chan-disclaimer-accepted';
|
export const DISCLAIMER_ACCEPTED_KEY = '5chan-disclaimer-accepted';
|
||||||
|
|
||||||
interface DisclaimerModalState {
|
interface DisclaimerModalState {
|
||||||
showModal: boolean;
|
showModal: boolean;
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import useBoardsFilterStore from '../../../stores/use-boards-filter-store';
|
||||||
|
import { DISCLAIMER_ACCEPTED_KEY } from '../../../stores/use-disclaimer-modal-store';
|
||||||
|
import styles from '../home.module.css';
|
||||||
|
|
||||||
|
const BoardsFilterModal = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [showFilterModal, setShowFilterModal] = useState(false);
|
||||||
|
const modalRef = useRef<HTMLDivElement>(null);
|
||||||
|
const buttonRef = useRef<HTMLSpanElement>(null);
|
||||||
|
|
||||||
|
const { useCatalogLinks, setUseCatalogLinks, boardFilter, setBoardFilter } = useBoardsFilterStore();
|
||||||
|
|
||||||
|
// Check if disclaimer has been accepted
|
||||||
|
const hasAcceptedDisclaimer = (): boolean => {
|
||||||
|
try {
|
||||||
|
return localStorage.getItem(DISCLAIMER_ACCEPTED_KEY) === 'true';
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const disclaimerAccepted = hasAcceptedDisclaimer();
|
||||||
|
|
||||||
|
const handleClickOutside = useCallback(
|
||||||
|
(event: MouseEvent) => {
|
||||||
|
if (modalRef.current && !modalRef.current.contains(event.target as Node) && buttonRef.current && !buttonRef.current.contains(event.target as Node)) {
|
||||||
|
setShowFilterModal(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[modalRef, buttonRef, setShowFilterModal],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, [handleClickOutside]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span ref={buttonRef} onClick={() => !showFilterModal && setShowFilterModal(true)}>
|
||||||
|
{t('filter')} ▼
|
||||||
|
</span>
|
||||||
|
{showFilterModal && (
|
||||||
|
<div ref={modalRef} className={styles.filterModal}>
|
||||||
|
{/* Always shown: Use Catalog */}
|
||||||
|
<div
|
||||||
|
className={`${styles.option} ${useCatalogLinks && styles.selected}`}
|
||||||
|
onClick={() => {
|
||||||
|
setUseCatalogLinks(!useCatalogLinks);
|
||||||
|
setShowFilterModal(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('use_catalog')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Conditionally shown: Filtering options (only after disclaimer accepted) */}
|
||||||
|
{disclaimerAccepted && (
|
||||||
|
<>
|
||||||
|
<div className={styles.separator} />
|
||||||
|
<div
|
||||||
|
className={`${styles.option} ${boardFilter === 'all' && styles.selected}`}
|
||||||
|
onClick={() => {
|
||||||
|
setBoardFilter('all');
|
||||||
|
setShowFilterModal(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('show_all_boards')}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`${styles.option} ${boardFilter === 'nsfw' && styles.selected}`}
|
||||||
|
onClick={() => {
|
||||||
|
setBoardFilter('nsfw');
|
||||||
|
setShowFilterModal(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('show_nsfw_boards_only')}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`${styles.option} ${boardFilter === 'worksafe' && styles.selected}`}
|
||||||
|
onClick={() => {
|
||||||
|
setBoardFilter('worksafe');
|
||||||
|
setShowFilterModal(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('show_worksafe_boards_only')}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BoardsFilterModal;
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
|
|
||||||
.boardsBox {
|
|
||||||
overflow: hidden;
|
|
||||||
min-height: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.boardsBox table {
|
|
||||||
display: table;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
table-layout: fixed;
|
|
||||||
width: calc(100% + 4px);
|
|
||||||
margin-left: -2px;
|
|
||||||
border-collapse: separate;
|
|
||||||
border-spacing: 2px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.boardsBox table thead th {
|
|
||||||
background: #fca;
|
|
||||||
color: #800;
|
|
||||||
border: 1px solid #800;
|
|
||||||
padding: 4px 15px 5px 5px;
|
|
||||||
text-align: left;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
||||||
|
|
||||||
.boardsBox table tbody td {
|
|
||||||
margin: 0;
|
|
||||||
padding: 4px 15px 4px 4px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.boardsBox table tbody tr:nth-of-type( even ) {
|
|
||||||
background: #ede2d4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.boardCell {
|
|
||||||
position: static;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.offlineIconContainer {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.offlineIcon {
|
|
||||||
vertical-align: text-top;
|
|
||||||
width: 13px;
|
|
||||||
height: 13px;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: contain;
|
|
||||||
image-rendering: pixelated;
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.boardCell a {
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loadingBoardCellValue {
|
|
||||||
color: var(--topbar-separator-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.loadMoreButton {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 14px;
|
|
||||||
color: var(--topbar-separator-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.displayCount {
|
|
||||||
font-size: 12px;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
text-transform: lowercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loadMoreButton span {
|
|
||||||
color: var(--topbar-desktop-link-text-color);
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loadMoreButton span:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--button-desktop-text-color-hover);
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nsfw {
|
|
||||||
color: red;
|
|
||||||
font-weight: 700;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: smaller;
|
|
||||||
}
|
|
||||||
|
|
||||||
.boardPPH {
|
|
||||||
width: 33px;
|
|
||||||
}
|
|
||||||
@@ -1,100 +1,273 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import Plebbit from '@plebbit/plebbit-js';
|
|
||||||
import { useDefaultSubplebbitsState, MultisubSubplebbit } from '../../../hooks/use-default-subplebbits';
|
import { useDefaultSubplebbitsState, MultisubSubplebbit } from '../../../hooks/use-default-subplebbits';
|
||||||
import useIsMobile from '../../../hooks/use-is-mobile';
|
|
||||||
import LoadingEllipsis from '../../../components/loading-ellipsis';
|
import LoadingEllipsis from '../../../components/loading-ellipsis';
|
||||||
import useDisclaimerModalStore from '../../../stores/use-disclaimer-modal-store';
|
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 }) => {
|
// Helper function to find board address by matching title pattern
|
||||||
const { t } = useTranslation();
|
const findBoardAddress = (multisub: MultisubSubplebbit[], titlePattern: string): string | null => {
|
||||||
const { address, title, nsfw } = subplebbit || {};
|
const entry = multisub.find((ms) => ms.title === titlePattern);
|
||||||
const boardTitle = title?.replace(/^\/[^/]+\/\s*-\s*/, '') || '';
|
return entry?.address || null;
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
const NSFWBadge = () => {
|
||||||
return (
|
return (
|
||||||
<tr key={address}>
|
<>
|
||||||
<td>
|
|
||||||
<p className={styles.boardCell}>
|
<h3 className={styles.nsfwBadge}>
|
||||||
<Link to={`/p/${address}`} onClick={handleLinkClick}>
|
<span title='Not Safe For Work'>
|
||||||
{displayAddress}
|
<sup>(NSFW)</sup>
|
||||||
</Link>
|
</span>
|
||||||
{nsfw && <span className={styles.nsfw}> ({t('nsfw')})</span>}
|
</h3>
|
||||||
</p>
|
</>
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<p className={styles.boardCell}>{boardTitle || displayAddress}</p>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const BoardsList = ({ multisub }: { multisub: MultisubSubplebbit[] }) => {
|
const BoardsList = ({ multisub }: { multisub: MultisubSubplebbit[] }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [displayCount, setDisplayCount] = useState(15);
|
const navigate = useNavigate();
|
||||||
const isMobile = useIsMobile();
|
|
||||||
const { loading, error } = useDefaultSubplebbitsState();
|
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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.boardsBox}>
|
<div className={styles.box}>
|
||||||
<span className={styles.loading}>
|
<div className={`${styles.boxBar} ${styles.color2ColorBar}`}>
|
||||||
|
<h2 className='capitalize'>{t('boards')}</h2>
|
||||||
|
<BoardsFilterModal />
|
||||||
|
</div>
|
||||||
|
<div className={styles.boxContent}>
|
||||||
<LoadingEllipsis string={t('loading_default_boards')} />
|
<LoadingEllipsis string={t('loading_default_boards')} />
|
||||||
</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.boardsBox}>
|
<div className={styles.box}>
|
||||||
<div className='red'>{error.message}</div>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredBoards = multisub.slice(0, displayCount);
|
// Find active boards
|
||||||
const totalBoardCount = multisub.length;
|
const bizAddress = findBoardAddress(multisub, '/biz/ - Business & Finance');
|
||||||
const hasMoreBoards = displayCount < multisub.length;
|
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 (
|
return (
|
||||||
<div className={styles.boardsBox}>
|
<div className={styles.box}>
|
||||||
<table className={styles.boardsList}>
|
<div className={`${styles.boxBar} ${styles.color2ColorBar}`}>
|
||||||
<colgroup>
|
<h2 className='capitalize'>{t('boards')}</h2>
|
||||||
<col className={styles.boardAddress} />
|
<BoardsFilterModal />
|
||||||
<col className={styles.boardTitle} />
|
</div>
|
||||||
</colgroup>
|
<div className={`${styles.boxContent} ${styles.boardsContent}`}>
|
||||||
<thead>
|
{/* Japanese Culture */}
|
||||||
<tr>
|
{showJapaneseCulture && (
|
||||||
<th>{t('board')}</th>
|
<div className={styles.boardsColumn}>
|
||||||
<th>{t('title')}</th>
|
<h3>Japanese Culture</h3>
|
||||||
</tr>
|
<ul>
|
||||||
</thead>
|
{/* Placeholder boards */}
|
||||||
<tbody>
|
<li>
|
||||||
{filteredBoards.map((sub) => (
|
<Link to='#' onClick={(e) => e.preventDefault()}>
|
||||||
<Board key={sub.address} subplebbit={sub} isMobile={isMobile} />
|
Anime & Manga
|
||||||
))}
|
</Link>
|
||||||
</tbody>
|
</li>
|
||||||
</table>
|
<li>
|
||||||
<div className={styles.displayCount}>
|
<Link to='#' onClick={(e) => e.preventDefault()}>
|
||||||
{t('displaying_boards', { filteredBoardsCount: filteredBoards.length, totalBoardCount: totalBoardCount, interpolation: { escapeValue: false } })}
|
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>
|
</div>
|
||||||
{hasMoreBoards && (
|
|
||||||
<div className={styles.loadMoreButton}>
|
|
||||||
[<span onClick={() => setDisplayCount((prevCount) => prevCount + 15)}>{t('load_more')}</span>]
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -157,7 +157,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.filterModal .selected::before {
|
.filterModal .selected::before {
|
||||||
content: '✔ ';
|
content: '✓ ';
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterModal .separator {
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
margin: 6px 0 4px;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer a {
|
.footer a {
|
||||||
@@ -246,6 +252,72 @@
|
|||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.boardsContent {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 0.5em;
|
||||||
|
padding-top: 0.25em;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boardsColumn {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 150px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boardsColumn h3:not(.nsfwBadge) {
|
||||||
|
display: inline;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 100%;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
text-transform: capitalize;
|
||||||
|
color: #800;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nsfwBadge {
|
||||||
|
display: inline;
|
||||||
|
color: #e00;
|
||||||
|
font-size: 100% !important;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: smaller;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nsfwBadge sup {
|
||||||
|
vertical-align: baseline;
|
||||||
|
position: relative;
|
||||||
|
top: -0.2em;
|
||||||
|
font-size: 85%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boardsColumn ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boardsColumn li {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boardsColumn a {
|
||||||
|
color: var(--homepage-box-link-text-color) !important;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.boardsColumn a:hover {
|
||||||
|
color: var(--homepage-box-link-text-color-hover) !important;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.content {
|
.content {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user