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_nsfw_content_only": "Show NSFW Content 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_all_content": "Show All Content",
|
||||
"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 { NavigateFunction } from 'react-router-dom';
|
||||
|
||||
const DISCLAIMER_ACCEPTED_KEY = '5chan-disclaimer-accepted';
|
||||
export const DISCLAIMER_ACCEPTED_KEY = '5chan-disclaimer-accepted';
|
||||
|
||||
interface DisclaimerModalState {
|
||||
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 { 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>
|
||||
<>
|
||||
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -157,7 +157,13 @@
|
||||
}
|
||||
|
||||
.filterModal .selected::before {
|
||||
content: '✔ ';
|
||||
content: '✓ ';
|
||||
}
|
||||
|
||||
.filterModal .separator {
|
||||
border-bottom: 1px solid #ccc;
|
||||
margin: 6px 0 4px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
@@ -246,6 +252,72 @@
|
||||
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) {
|
||||
.content {
|
||||
min-width: 0;
|
||||
|
||||
Reference in New Issue
Block a user