From a9690ac0befba357c41595c05900efe1aa5a337e Mon Sep 17 00:00:00 2001 From: plebeius Date: Wed, 5 Nov 2025 19:50:31 +0100 Subject: [PATCH 01/12] feat(home): redesign boards list to match 4chan with filtering and catalog support --- public/translations/en/default.json | 2 +- src/stores/use-boards-filter-store.ts | 51 +++ src/stores/use-disclaimer-modal-store.ts | 2 +- .../home/boards-list/boards-filter-modal.tsx | 99 ++++++ .../home/boards-list/boards-list.module.css | 104 ------ src/views/home/boards-list/boards-list.tsx | 305 ++++++++++++++---- src/views/home/home.module.css | 74 ++++- 7 files changed, 464 insertions(+), 173 deletions(-) create mode 100644 src/stores/use-boards-filter-store.ts create mode 100644 src/views/home/boards-list/boards-filter-modal.tsx delete mode 100644 src/views/home/boards-list/boards-list.module.css diff --git a/public/translations/en/default.json b/public/translations/en/default.json index 15c8f5a0..1cd2de9f 100644 --- a/public/translations/en/default.json +++ b/public/translations/en/default.json @@ -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", diff --git a/src/stores/use-boards-filter-store.ts b/src/stores/use-boards-filter-store.ts new file mode 100644 index 00000000..b7c23b60 --- /dev/null +++ b/src/stores/use-boards-filter-store.ts @@ -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((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; diff --git a/src/stores/use-disclaimer-modal-store.ts b/src/stores/use-disclaimer-modal-store.ts index 89bc3240..7815964d 100644 --- a/src/stores/use-disclaimer-modal-store.ts +++ b/src/stores/use-disclaimer-modal-store.ts @@ -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; diff --git a/src/views/home/boards-list/boards-filter-modal.tsx b/src/views/home/boards-list/boards-filter-modal.tsx new file mode 100644 index 00000000..37b3da66 --- /dev/null +++ b/src/views/home/boards-list/boards-filter-modal.tsx @@ -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(null); + const buttonRef = useRef(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 ( + <> + !showFilterModal && setShowFilterModal(true)}> + {t('filter')} ▼ + + {showFilterModal && ( +
+ {/* Always shown: Use Catalog */} +
{ + setUseCatalogLinks(!useCatalogLinks); + setShowFilterModal(false); + }} + > + {t('use_catalog')} +
+ + {/* Conditionally shown: Filtering options (only after disclaimer accepted) */} + {disclaimerAccepted && ( + <> +
+
{ + setBoardFilter('all'); + setShowFilterModal(false); + }} + > + {t('show_all_boards')} +
+
{ + setBoardFilter('nsfw'); + setShowFilterModal(false); + }} + > + {t('show_nsfw_boards_only')} +
+
{ + setBoardFilter('worksafe'); + setShowFilterModal(false); + }} + > + {t('show_worksafe_boards_only')} +
+ + )} +
+ )} + + ); +}; + +export default BoardsFilterModal; diff --git a/src/views/home/boards-list/boards-list.module.css b/src/views/home/boards-list/boards-list.module.css deleted file mode 100644 index 19b0daaa..00000000 --- a/src/views/home/boards-list/boards-list.module.css +++ /dev/null @@ -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; -} diff --git a/src/views/home/boards-list/boards-list.tsx b/src/views/home/boards-list/boards-list.tsx index 7ea7b2c2..6d4d634d 100644 --- a/src/views/home/boards-list/boards-list.tsx +++ b/src/views/home/boards-list/boards-list.tsx @@ -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) => { - 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 ( - - -

- - {displayAddress} - - {nsfw && ({t('nsfw')})} -

- - -

{boardTitle || displayAddress}

- - + <> +   +

+ + (NSFW) + +

+ ); }; 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, 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 ( -
- +
+
+

{t('boards')}

+ +
+
- +
); } if (error) { return ( -
-
{error.message}
+
+
+

{t('boards')}

+ +
+
+
{error.message}
+
); } - 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 ( -
- - - - - - - - - - - - - {filteredBoards.map((sub) => ( - - ))} - -
{t('board')}{t('title')}
-
- {t('displaying_boards', { filteredBoardsCount: filteredBoards.length, totalBoardCount: totalBoardCount, interpolation: { escapeValue: false } })} +
+
+

{t('boards')}

+ +
+
+ {/* Japanese Culture */} + {showJapaneseCulture && ( +
+

Japanese Culture

+
    + {/* Placeholder boards */} +
  • + e.preventDefault()}> + Anime & Manga + +
  • +
  • + e.preventDefault()}> + Anime/Cute + +
  • +
  • + e.preventDefault()}> + Anime/Wallpapers + +
  • +
+
+ )} + + {/* Video Games */} + {showVideoGames && ( +
+

Video Games

+
    + {/* Placeholder boards */} +
  • + e.preventDefault()}> + Video Games + +
  • +
  • + e.preventDefault()}> + Video Game Generals + +
  • +
+
+ )} + + {/* Interests */} + {showInterests && ( +
+

Interests

+
    + {/* Placeholder boards */} +
  • + e.preventDefault()}> + Technology + +
  • +
  • + e.preventDefault()}> + Comics & Cartoons + +
  • +
  • + e.preventDefault()}> + Television & Film + +
  • +
+
+ )} + + {/* Creative */} + {showCreative && ( +
+

Creative

+
    + {/* Placeholder boards */} +
  • + e.preventDefault()}> + Photography + +
  • +
  • + e.preventDefault()}> + Literature + +
  • +
  • + e.preventDefault()}> + Music + +
  • +
+
+ )} + + {/* Other - ACTIVE: biz */} + {showOther && ( +
+

Other

+
    + {bizAddress && ( +
  • + handleLinkClick(e, bizAddress)}> + Business & Finance + +
  • + )} + {/* Placeholder boards */} +
  • + e.preventDefault()}> + Travel + +
  • +
  • + e.preventDefault()}> + Fitness + +
  • +
+
+ )} + + {/* Misc. (NSFW) - ACTIVE: pol */} + {showMisc && ( +
+

Misc.

+ +
    + {/* Placeholder boards */} +
  • + e.preventDefault()}> + Random + +
  • +
  • + e.preventDefault()}> + ROBOT9001 + +
  • + {polAddress && ( +
  • + handleLinkClick(e, polAddress)}> + Politically Incorrect + +
  • + )} +
+
+ )} + + {/* Adult (NSFW) */} + {showAdult && ( +
+

Adult

+ +
    + {/* Placeholder boards */} +
  • + e.preventDefault()}> + Sexy Beautiful Women + +
  • +
  • + e.preventDefault()}> + Hardcore + +
  • +
+
+ )}
- {hasMoreBoards && ( -
- [ setDisplayCount((prevCount) => prevCount + 15)}>{t('load_more')}] -
- )}
); }; diff --git a/src/views/home/home.module.css b/src/views/home/home.module.css index 5a06baa6..873d0c72 100644 --- a/src/views/home/home.module.css +++ b/src/views/home/home.module.css @@ -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; From 8de4fba9ed92e9dcd2b506bef898f35e3129e507 Mon Sep 17 00:00:00 2001 From: plebeius Date: Wed, 5 Nov 2025 19:55:11 +0100 Subject: [PATCH 02/12] fix: remove duplicate .filterModal .separator CSS rule --- src/views/home/home.module.css | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/views/home/home.module.css b/src/views/home/home.module.css index 873d0c72..3c3b1541 100644 --- a/src/views/home/home.module.css +++ b/src/views/home/home.module.css @@ -160,12 +160,6 @@ content: '✓ '; } -.filterModal .separator { - border-bottom: 1px solid #ccc; - margin: 6px 0 4px; - padding: 0; -} - .footer a { text-transform: capitalize; } From 43a46be904c89f33e5d66adbc48335ae9e99fadc Mon Sep 17 00:00:00 2001 From: plebeius Date: Wed, 5 Nov 2025 20:14:11 +0100 Subject: [PATCH 03/12] refactor(home): make boards list layout identical to 4chan --- src/views/home/boards-list/boards-list.tsx | 520 ++++++++++++++++----- src/views/home/home.module.css | 23 +- 2 files changed, 425 insertions(+), 118 deletions(-) diff --git a/src/views/home/boards-list/boards-list.tsx b/src/views/home/boards-list/boards-list.tsx index 6d4d634d..7a9b53ad 100644 --- a/src/views/home/boards-list/boards-list.tsx +++ b/src/views/home/boards-list/boards-list.tsx @@ -97,173 +97,473 @@ const BoardsList = ({ multisub }: { multisub: MultisubSubplebbit[] }) => {
- {/* Japanese Culture */} - {showJapaneseCulture && ( + {/* Column 1: Japanese Culture + Video Games */} + {(showJapaneseCulture || showVideoGames) && (
-

Japanese Culture

-
    - {/* Placeholder boards */} -
  • - e.preventDefault()}> - Anime & Manga - -
  • -
  • - e.preventDefault()}> - Anime/Cute - -
  • -
  • - e.preventDefault()}> - Anime/Wallpapers - -
  • -
+ {/* Japanese Culture */} + {showJapaneseCulture && ( + <> +

Japanese Culture

+
    +
  • + e.preventDefault()} className={styles.placeholder}> + Anime & Manga + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Anime/Cute + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Anime/Wallpapers + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Mecha + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Cosplay & EGL + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Cute/Male + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Flash + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Transportation + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Otaku Culture + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Virtual YouTubers + +
  • +
+ + )} + + {/* Video Games */} + {showVideoGames && ( + <> +

Video Games

+
    +
  • + e.preventDefault()} className={styles.placeholder}> + Video Games + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Video Game Generals + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Video Games/Multiplayer + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Video Games/Mobile + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Pokémon + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Retro Games + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Video Games/RPG + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Video Games/Strategy + +
  • +
+ + )}
)} - {/* Video Games */} - {showVideoGames && ( -
-

Video Games

-
    - {/* Placeholder boards */} -
  • - e.preventDefault()}> - Video Games - -
  • -
  • - e.preventDefault()}> - Video Game Generals - -
  • -
-
- )} - - {/* Interests */} + {/* Column 2: Interests */} {showInterests && (

Interests

    - {/* Placeholder boards */}
  • - e.preventDefault()}> - Technology - -
  • -
  • - e.preventDefault()}> + e.preventDefault()} className={styles.placeholder}> Comics & Cartoons
  • - e.preventDefault()}> + e.preventDefault()} className={styles.placeholder}> + Technology + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> Television & Film
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Weapons + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Auto + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Animals & Nature + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Traditional Games + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Sports + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Extreme Sports + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Professional Wrestling + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Science & Math + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + History & Humanities + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + International + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Outdoors + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Toys + +
)} - {/* Creative */} + {/* Column 3: Creative */} {showCreative && (

Creative

    - {/* Placeholder boards */}
  • - e.preventDefault()}> + e.preventDefault()} className={styles.placeholder}> + Oekaki + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Papercraft & Origami + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> Photography
  • - e.preventDefault()}> + e.preventDefault()} className={styles.placeholder}> + Food & Cooking + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Artwork/Critique + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Wallpapers/General + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> Literature
  • - e.preventDefault()}> + e.preventDefault()} className={styles.placeholder}> Music
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Fashion + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + 3DCG + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Graphic Design + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Do-It-Yourself + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Worksafe GIF + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Quests + +
)} - {/* Other - ACTIVE: biz */} - {showOther && ( + {/* Column 4: Other + Misc. */} + {(showOther || showMisc) && (
-

Other

-
    - {bizAddress && ( -
  • - handleLinkClick(e, bizAddress)}> - Business & Finance - -
  • - )} - {/* Placeholder boards */} -
  • - e.preventDefault()}> - Travel - -
  • -
  • - e.preventDefault()}> - Fitness - -
  • -
+ {/* Other */} + {showOther && ( + <> +

Other

+
    + {bizAddress ? ( +
  • + handleLinkClick(e, bizAddress)}> + Business & Finance + +
  • + ) : ( +
  • + e.preventDefault()} className={styles.placeholder}> + Business & Finance + +
  • + )} +
  • + e.preventDefault()} className={styles.placeholder}> + Travel + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Fitness + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Paranormal + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Advice + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + LGBT + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Pony + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Current News + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Worksafe Requests + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Very Important Posts + +
  • +
+ + )} + + {/* Misc. (NSFW) */} + {showMisc && ( + <> +

Misc.

+ +
    +
  • + e.preventDefault()} className={styles.placeholder}> + Random + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + ROBOT9001 + +
  • + {polAddress ? ( +
  • + handleLinkClick(e, polAddress)}> + Politically Incorrect + +
  • + ) : ( +
  • + e.preventDefault()} className={styles.placeholder}> + Politically Incorrect + +
  • + )} +
  • + e.preventDefault()} className={styles.placeholder}> + International/Random + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Cams & Meetups + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Shit 4chan Says + +
  • +
+ + )}
)} - {/* Misc. (NSFW) - ACTIVE: pol */} - {showMisc && ( -
-

Misc.

- -
    - {/* Placeholder boards */} -
  • - e.preventDefault()}> - Random - -
  • -
  • - e.preventDefault()}> - ROBOT9001 - -
  • - {polAddress && ( -
  • - handleLinkClick(e, polAddress)}> - Politically Incorrect - -
  • - )} -
-
- )} - - {/* Adult (NSFW) */} + {/* Column 5: Adult (NSFW) */} {showAdult && (

Adult

    - {/* Placeholder boards */}
  • - e.preventDefault()}> + e.preventDefault()} className={styles.placeholder}> Sexy Beautiful Women
  • - e.preventDefault()}> + e.preventDefault()} className={styles.placeholder}> Hardcore
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Handsome Men + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Hentai + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Ecchi + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Yuri + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Hentai/Alternative + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Yaoi + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Torrents + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + High Resolution + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Adult GIF + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Adult Cartoons + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Adult Requests + +
)} diff --git a/src/views/home/home.module.css b/src/views/home/home.module.css index 3c3b1541..9556507c 100644 --- a/src/views/home/home.module.css +++ b/src/views/home/home.module.css @@ -247,18 +247,22 @@ } .boardsContent { - display: flex; - flex-wrap: wrap; - gap: 20px; + display: block; padding: 0.5em; padding-top: 0.25em; padding-bottom: 0; } +.boardsContent::after { + content: ""; + display: table; + clear: both; +} + .boardsColumn { - flex: 1; - min-width: 150px; - vertical-align: top; + display: block; + float: left; + width: 145px; } .boardsColumn h3:not(.nsfwBadge) { @@ -293,13 +297,12 @@ list-style: none; padding: 0; margin: 0; - margin-bottom: 15px; } .boardsColumn li { margin: 0; padding: 0; - line-height: 1.5em; + line-height: 130%; } .boardsColumn a { @@ -312,6 +315,10 @@ text-decoration: underline; } +.boardsColumn a.placeholder { + text-decoration: line-through; +} + @media (max-width: 640px) { .content { min-width: 0; From cc88d805f4cbdc3f9695cee3410815cb5bb3e356 Mon Sep 17 00:00:00 2001 From: plebeius Date: Wed, 5 Nov 2025 22:33:50 +0100 Subject: [PATCH 04/12] fix body font size --- src/themes.css | 8 ++++---- src/views/home/home.module.css | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/themes.css b/src/themes.css index 3da8c039..a9a20642 100644 --- a/src/themes.css +++ b/src/themes.css @@ -23,7 +23,7 @@ --body-background-pattern-image: url("/assets/background-fade.png"); --body-font-color: maroon; --body-font-family: arial, helvetica, sans-serif; - --body-font-size: 10pt; + --body-font-size: 13px; /* catalog post */ --catalog-post-menu-desktop-btn-color: navy; @@ -239,7 +239,7 @@ --body-background-pattern-image: url("/assets/background-fade-blue.png"); --body-font-color: #000; --body-font-family: arial, helvetica, sans-serif; - --body-font-size: 10pt; + --body-font-size: 13px; /* catalog post */ --catalog-post-menu-desktop-btn-color: navy; @@ -819,7 +819,7 @@ --body-background-pattern-image: none; --body-font-color: #c5c8c6; --body-font-family: arial, helvetica, sans-serif; - --body-font-size: 10pt; + --body-font-size: 13px; /* board banner */ --banner-image-border: 1px solid #000; @@ -1040,7 +1040,7 @@ --body-background-pattern-image: none; --body-font-color: #333; --body-font-family: arial, helvetica, sans-serif; - --body-font-size: 10pt; + --body-font-size: 13px; /* board banner */ --banner-image-border: 1px solid #000; diff --git a/src/views/home/home.module.css b/src/views/home/home.module.css index 9556507c..d8b13367 100644 --- a/src/views/home/home.module.css +++ b/src/views/home/home.module.css @@ -270,7 +270,6 @@ font-weight: 700; font-size: 100%; margin: 0; - margin-bottom: 5px; text-transform: capitalize; color: #800; text-decoration: underline; From eedb0fa08eb71c98d51e1068e79df8a4a2f48f9a Mon Sep 17 00:00:00 2001 From: plebeius Date: Thu, 6 Nov 2025 11:23:06 +0100 Subject: [PATCH 05/12] fix: prevent memory leak from unbounded setInterval in use-time-filter --- .../reply-quote-preview.tsx | 6 ++++-- src/hooks/use-time-filter.ts | 19 +++++++++++++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/components/reply-quote-preview/reply-quote-preview.tsx b/src/components/reply-quote-preview/reply-quote-preview.tsx index c0fa61d1..2d2a71a0 100644 --- a/src/components/reply-quote-preview/reply-quote-preview.tsx +++ b/src/components/reply-quote-preview/reply-quote-preview.tsx @@ -194,9 +194,11 @@ const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, is }); useEffect(() => { - window.addEventListener('resize', () => update()); + // Create a stable function reference for proper cleanup + const handleResize = () => update(); + window.addEventListener('resize', handleResize); return () => { - window.removeEventListener('resize', () => update()); + window.removeEventListener('resize', handleResize); }; }, [update]); diff --git a/src/hooks/use-time-filter.ts b/src/hooks/use-time-filter.ts index f8a03ba1..ccfda5b5 100644 --- a/src/hooks/use-time-filter.ts +++ b/src/hooks/use-time-filter.ts @@ -4,10 +4,21 @@ import { useParams } from 'react-router-dom'; // the timestamp the last time the user visited const lastVisitTimestamp = localStorage.getItem('5chanLastVisitTimestamp'); -// update the last visited timestamp every n seconds -setInterval(() => { - localStorage.setItem('5chanLastVisitTimestamp', Date.now().toString()); -}, 60 * 1000); +// Singleton pattern to ensure only one interval is ever created +// This prevents memory leaks from multiple intervals being created during HMR or module re-evaluation +// Using window object to persist across hot module reloads +declare global { + interface Window { + _5chanLastVisitTimestampIntervalId?: number; + } +} + +if (!window._5chanLastVisitTimestampIntervalId) { + // update the last visited timestamp every n seconds + window._5chanLastVisitTimestampIntervalId = window.setInterval(() => { + localStorage.setItem('5chanLastVisitTimestamp', Date.now().toString()); + }, 60 * 1000); +} const timeFilterNamesToSeconds: Record = { '1h': 60 * 60, From c5c364faeaff2914ee9c44072453de328340c123 Mon Sep 17 00:00:00 2001 From: plebeius Date: Thu, 6 Nov 2025 11:43:31 +0100 Subject: [PATCH 06/12] adjust styling --- src/views/home/home.module.css | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/views/home/home.module.css b/src/views/home/home.module.css index d8b13367..9ed7da21 100644 --- a/src/views/home/home.module.css +++ b/src/views/home/home.module.css @@ -3,8 +3,8 @@ height: 100%; margin: auto; text-align: left; - width: 750px; - min-width: 750px; + width: 748px; + min-width: 748px; } .logo img { @@ -251,6 +251,7 @@ padding: 0.5em; padding-top: 0.25em; padding-bottom: 0; + line-height: 130%; } .boardsContent::after { @@ -286,10 +287,8 @@ } .nsfwBadge sup { - vertical-align: baseline; - position: relative; - top: -0.2em; - font-size: 85%; + vertical-align: text-bottom; + font-size: smaller; } .boardsColumn ul { @@ -349,7 +348,7 @@ @media (min-width: 640px) { .footer { border-top: 1px solid; - width: 750px; + width: 748px; display: inline-flex; justify-content: center; font-size: 12px; From 36bd9c5c0f6044655ae99f9fa34b34615af2f644 Mon Sep 17 00:00:00 2001 From: plebeius Date: Thu, 6 Nov 2025 15:52:45 +0100 Subject: [PATCH 07/12] feat(boards-list): mark Flash, Oekaki, Artwork/Critique, Wallpapers/General as NSFW --- src/views/home/boards-list/boards-list.tsx | 268 +++++++++++---------- 1 file changed, 146 insertions(+), 122 deletions(-) diff --git a/src/views/home/boards-list/boards-list.tsx b/src/views/home/boards-list/boards-list.tsx index 7a9b53ad..e30ede60 100644 --- a/src/views/home/boards-list/boards-list.tsx +++ b/src/views/home/boards-list/boards-list.tsx @@ -82,10 +82,10 @@ const BoardsList = ({ multisub }: { multisub: MultisubSubplebbit[] }) => { const showWorksafeOnly = boardFilter === 'worksafe'; // Category visibility based on filter - const showJapaneseCulture = showAll || showWorksafeOnly; + const showJapaneseCulture = showAll || showWorksafeOnly || showNsfwOnly; const showVideoGames = showAll || showWorksafeOnly; const showInterests = showAll || showWorksafeOnly; - const showCreative = showAll || showWorksafeOnly; + const showCreative = showAll || showWorksafeOnly || showNsfwOnly; const showOther = showAll || showWorksafeOnly; const showMisc = showAll || showNsfwOnly; const showAdult = showAll || showNsfwOnly; @@ -105,56 +105,66 @@ const BoardsList = ({ multisub }: { multisub: MultisubSubplebbit[] }) => { <>

Japanese Culture

    -
  • - e.preventDefault()} className={styles.placeholder}> - Anime & Manga - -
  • -
  • - e.preventDefault()} className={styles.placeholder}> - Anime/Cute - -
  • -
  • - e.preventDefault()} className={styles.placeholder}> - Anime/Wallpapers - -
  • -
  • - e.preventDefault()} className={styles.placeholder}> - Mecha - -
  • -
  • - e.preventDefault()} className={styles.placeholder}> - Cosplay & EGL - -
  • -
  • - e.preventDefault()} className={styles.placeholder}> - Cute/Male - -
  • -
  • - e.preventDefault()} className={styles.placeholder}> - Flash - -
  • -
  • - e.preventDefault()} className={styles.placeholder}> - Transportation - -
  • -
  • - e.preventDefault()} className={styles.placeholder}> - Otaku Culture - -
  • -
  • - e.preventDefault()} className={styles.placeholder}> - Virtual YouTubers - -
  • + {(showAll || showWorksafeOnly) && ( + <> +
  • + e.preventDefault()} className={styles.placeholder}> + Anime & Manga + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Anime/Cute + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Anime/Wallpapers + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Mecha + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Cosplay & EGL + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Cute/Male + +
  • + + )} + {(showAll || showNsfwOnly) && ( +
  • + e.preventDefault()} className={styles.placeholder}> + Flash + +
  • + )} + {(showAll || showWorksafeOnly) && ( + <> +
  • + e.preventDefault()} className={styles.placeholder}> + Transportation + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Otaku Culture + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Virtual YouTubers + +
  • + + )}
)} @@ -299,76 +309,90 @@ const BoardsList = ({ multisub }: { multisub: MultisubSubplebbit[] }) => {

Creative

    -
  • - e.preventDefault()} className={styles.placeholder}> - Oekaki - -
  • -
  • - e.preventDefault()} className={styles.placeholder}> - Papercraft & Origami - -
  • -
  • - e.preventDefault()} className={styles.placeholder}> - Photography - -
  • -
  • - e.preventDefault()} className={styles.placeholder}> - Food & Cooking - -
  • -
  • - e.preventDefault()} className={styles.placeholder}> - Artwork/Critique - -
  • -
  • - e.preventDefault()} className={styles.placeholder}> - Wallpapers/General - -
  • -
  • - e.preventDefault()} className={styles.placeholder}> - Literature - -
  • -
  • - e.preventDefault()} className={styles.placeholder}> - Music - -
  • -
  • - e.preventDefault()} className={styles.placeholder}> - Fashion - -
  • -
  • - e.preventDefault()} className={styles.placeholder}> - 3DCG - -
  • -
  • - e.preventDefault()} className={styles.placeholder}> - Graphic Design - -
  • -
  • - e.preventDefault()} className={styles.placeholder}> - Do-It-Yourself - -
  • -
  • - e.preventDefault()} className={styles.placeholder}> - Worksafe GIF - -
  • -
  • - e.preventDefault()} className={styles.placeholder}> - Quests - -
  • + {(showAll || showNsfwOnly) && ( +
  • + e.preventDefault()} className={styles.placeholder}> + Oekaki + +
  • + )} + {(showAll || showWorksafeOnly) && ( + <> +
  • + e.preventDefault()} className={styles.placeholder}> + Papercraft & Origami + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Photography + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Food & Cooking + +
  • + + )} + {(showAll || showNsfwOnly) && ( +
  • + e.preventDefault()} className={styles.placeholder}> + Artwork/Critique + +
  • + )} + {(showAll || showNsfwOnly) && ( +
  • + e.preventDefault()} className={styles.placeholder}> + Wallpapers/General + +
  • + )} + {(showAll || showWorksafeOnly) && ( + <> +
  • + e.preventDefault()} className={styles.placeholder}> + Literature + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Music + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Fashion + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + 3DCG + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Graphic Design + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Do-It-Yourself + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Worksafe GIF + +
  • +
  • + e.preventDefault()} className={styles.placeholder}> + Quests + +
  • + + )}
)} From e627b6b619676fa877424bf53da971a99815ebab Mon Sep 17 00:00:00 2001 From: plebeius Date: Thu, 6 Nov 2025 16:23:55 +0100 Subject: [PATCH 08/12] feat(directory-modal): add modal explaining how to submit boards to directories --- .../directory-modal.module.css | 119 +++++++++++++ .../directory-modal/directory-modal.tsx | 105 ++++++++++++ src/components/directory-modal/index.ts | 1 + src/components/topbar/topbar.tsx | 14 +- src/stores/use-directory-modal-store.ts | 25 +++ src/views/home/boards-list/boards-list.tsx | 160 +++++++++--------- src/views/home/home.tsx | 2 + 7 files changed, 340 insertions(+), 86 deletions(-) create mode 100644 src/components/directory-modal/directory-modal.module.css create mode 100644 src/components/directory-modal/directory-modal.tsx create mode 100644 src/components/directory-modal/index.ts create mode 100644 src/stores/use-directory-modal-store.ts diff --git a/src/components/directory-modal/directory-modal.module.css b/src/components/directory-modal/directory-modal.module.css new file mode 100644 index 00000000..c98312e8 --- /dev/null +++ b/src/components/directory-modal/directory-modal.module.css @@ -0,0 +1,119 @@ +.backdrop { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(128, 128, 128, 0.5); + z-index: 999; +} + +.directoryDialog { + position: absolute; + width: 500px; + top: 50px; + left: 50%; + margin-left: -251px; + background-color: rgb(255, 255, 255); + border: 1px solid rgb(136, 0, 0); + color: rgb(136, 0, 0); + box-shadow: rgba(0, 0, 0, 0.1) 2px 2px 0px 1px; + font-family: arial, helvetica, clean, sans-serif; + font-size: 13px; + line-height: 16.003px; + z-index: 1000; +} + +@media (max-width: 600px) { + .directoryDialog { + width: calc(100% - 20px); + margin-left: -50%; + left: 50%; + max-width: 500px; + } +} + +.hd { + position: relative; + background-color: rgb(136, 0, 0); + color: rgb(255, 255, 255); + padding: 4px 6px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.hd h2 { + margin: 0; + padding: 0; + font-size: 131%; + font-weight: bold; +} + +.closeButton { + all: unset; + cursor: pointer; + width: 16px; + height: 16px; + position: absolute; + top: 4px; + right: 4px; + background-image: var(--disclaimer-modal-close-button-background-image); + background-size: 100%; + background-position: center; + background-repeat: no-repeat; +} + +.bd { + padding: 10px; +} + +.introMessage { + font-size: 15px; + margin: 10px 0 20px 0; + text-align: center; +} + +.introMessage strong { + font-weight: bold; +} + +.section { + margin-bottom: 20px; +} + +.section:last-child { + margin-bottom: 0; +} + +.section h3 { + margin: 0 0 8px 0; + padding: 0; + font-size: 13px; + font-weight: bold; + color: rgb(136, 0, 0); +} + +.section p { + margin: 0 0 12px 0; + font-size: 13px; + line-height: 16.003px; +} + +.section p:last-child { + margin-bottom: 0; +} + +.bd a { + color: rgb(136, 0, 0); + text-decoration: underline; +} + +.directoryFooter { + padding: 10px; + display: flex; + justify-content: center; + gap: 10px; +} + + diff --git a/src/components/directory-modal/directory-modal.tsx b/src/components/directory-modal/directory-modal.tsx new file mode 100644 index 00000000..527b6d02 --- /dev/null +++ b/src/components/directory-modal/directory-modal.tsx @@ -0,0 +1,105 @@ +import useDirectoryModalStore from '../../stores/use-directory-modal-store'; +import styles from './directory-modal.module.css'; + +const DirectoryModal = () => { + const { showModal, closeDirectoryModal } = useDirectoryModalStore(); + + if (!showModal) { + return null; + } + + const handleBackdropClick = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + closeDirectoryModal(); + } + }; + + return ( +
+
+
+

Submit a Board to a Directory

+
+
+

+ The board you clicked on doesn't exist yet, but it can be yours! +

+ +
+

What is a directory board vs a regular board?

+

+ On 5chan, anyone can create and connect to any board using its address. A "directory board" is simply a board that has been assigned to a specific + directory category (like "Anime & Manga" or "Video Games") in the boards list on the homepage. This assignment is temporary and + handpicked by the devs until DAO curation is implemented. +

+

+ Important: Anyone can create a board and users can access it at any time, regardless of whether it's assigned to a directory or not. + You can use the search bar on the homepage to connect to any board address peer-to-peer, even if it's not added as a directory. Additionally, every + board has a "[Subscribe]" button—when you subscribe to a board, it will appear in the top bar on the boards page, making it easy to access your + favorite boards regardless of directory assignment. +

+
+ +
+

How to create your own board

+

+ You can create a board using either the{' '} + + Seedit GUI client + {' '} + or the{' '} + + plebbit-cli command line interface + + . Once created, anyone can connect to your board using its address, regardless of whether it's assigned to a directory or not. +

+
+ +
+

Requirements for directory assignment

+

+ To have your board assigned to a directory, it should be active, well-moderated, and relevant to the directory category. Most importantly, it should have + 99% uptime, since a board acts like its own server (it's a P2P node). The devs review submissions and reserve the right to approve or reject them based + on these criteria. +

+
+ +
+

How to submit your board

+

+ To request your board be added to a directory, open a pull request on GitHub by editing the{' '} + + 5chan-multisub.json file + + . Add your board's entry with its title, address, and NSFW status (if applicable). The devs will review your PR and merge it if approved. +

+
+ +
+

Future roadmap

+

+ In the future, directory board assignments will be determined through gasless voting using pubsub. Community members will vote on which board should be + assigned to each directory, and the highest voted board will automatically become the directory board. This will make the process fully decentralized and + community-driven. +

+
+ +
+

Dev veto power

+

+ While the devs reserve the right to change directory resolutions, this is done through commits in the fully open-source repository. There is no centralized + or server-side action required—anyone can change the directory resolution by modifying the code and redeploying to their own domain. 5chan is adminless, and + the app itself has no owner or central authority. +

+
+
+
+ +
+
+
+ ); +}; + +export default DirectoryModal; diff --git a/src/components/directory-modal/index.ts b/src/components/directory-modal/index.ts new file mode 100644 index 00000000..08c007a5 --- /dev/null +++ b/src/components/directory-modal/index.ts @@ -0,0 +1 @@ +export { default } from './directory-modal'; diff --git a/src/components/topbar/topbar.tsx b/src/components/topbar/topbar.tsx index 246f508c..519f015c 100644 --- a/src/components/topbar/topbar.tsx +++ b/src/components/topbar/topbar.tsx @@ -6,6 +6,7 @@ import { useAccount, useAccountComment, useAccountSubplebbits } from '@plebbit/p import { isAllView, isCatalogView, isSubscriptionsView } from '../../lib/utils/view-utils'; import { useDefaultSubplebbitAddresses } from '../../hooks/use-default-subplebbits'; import { TimeFilter } from '../board-buttons'; +import useDirectoryModalStore from '../../stores/use-directory-modal-store'; import styles from './topbar.module.css'; import _, { debounce } from 'lodash'; @@ -75,6 +76,7 @@ const TopBarDesktop = () => { const params = useParams(); const isInCatalogView = isCatalogView(location.pathname, params); const [showSearchBar, setShowSearchBar] = useState(false); + const { openDirectoryModal } = useDirectoryModalStore(); const subscriptions = account?.subscriptions; @@ -108,16 +110,8 @@ const TopBarDesktop = () => { )} [ - - - {t('create_board')} - - - ] [ - - - {t('vote')} - + + {t('create_board')} ] diff --git a/src/stores/use-directory-modal-store.ts b/src/stores/use-directory-modal-store.ts new file mode 100644 index 00000000..873aae9c --- /dev/null +++ b/src/stores/use-directory-modal-store.ts @@ -0,0 +1,25 @@ +import { create } from 'zustand'; + +interface DirectoryModalState { + showModal: boolean; + openDirectoryModal: () => void; + closeDirectoryModal: () => void; +} + +const useDirectoryModalStore = create((set) => ({ + showModal: false, + + openDirectoryModal: () => { + set({ + showModal: true, + }); + }, + + closeDirectoryModal: () => { + set({ + showModal: false, + }); + }, +})); + +export default useDirectoryModalStore; diff --git a/src/views/home/boards-list/boards-list.tsx b/src/views/home/boards-list/boards-list.tsx index e30ede60..121b9c7a 100644 --- a/src/views/home/boards-list/boards-list.tsx +++ b/src/views/home/boards-list/boards-list.tsx @@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'; import { useDefaultSubplebbitsState, MultisubSubplebbit } from '../../../hooks/use-default-subplebbits'; import LoadingEllipsis from '../../../components/loading-ellipsis'; import useDisclaimerModalStore from '../../../stores/use-disclaimer-modal-store'; +import useDirectoryModalStore from '../../../stores/use-directory-modal-store'; import useBoardsFilterStore from '../../../stores/use-boards-filter-store'; import BoardsFilterModal from './boards-filter-modal'; import styles from '../home.module.css'; @@ -31,6 +32,7 @@ const BoardsList = ({ multisub }: { multisub: MultisubSubplebbit[] }) => { const navigate = useNavigate(); const { loading, error } = useDefaultSubplebbitsState(); const { showDisclaimerModal } = useDisclaimerModalStore(); + const { openDirectoryModal } = useDirectoryModalStore(); const { useCatalogLinks, boardFilter } = useBoardsFilterStore(); const handleLinkClick = (e: React.MouseEvent, address: string) => { @@ -44,6 +46,12 @@ const BoardsList = ({ multisub }: { multisub: MultisubSubplebbit[] }) => { return `/p/${address}${useCatalogLinks ? '/catalog' : ''}`; }; + // Handler for placeholder board links + const handlePlaceholderClick = (e: React.MouseEvent) => { + e.preventDefault(); + openDirectoryModal(); + }; + if (loading) { return (
@@ -108,32 +116,32 @@ const BoardsList = ({ multisub }: { multisub: MultisubSubplebbit[] }) => { {(showAll || showWorksafeOnly) && ( <>
  • - e.preventDefault()} className={styles.placeholder}> + Anime & Manga
  • - e.preventDefault()} className={styles.placeholder}> + Anime/Cute
  • - e.preventDefault()} className={styles.placeholder}> + Anime/Wallpapers
  • - e.preventDefault()} className={styles.placeholder}> + Mecha
  • - e.preventDefault()} className={styles.placeholder}> + Cosplay & EGL
  • - e.preventDefault()} className={styles.placeholder}> + Cute/Male
  • @@ -141,7 +149,7 @@ const BoardsList = ({ multisub }: { multisub: MultisubSubplebbit[] }) => { )} {(showAll || showNsfwOnly) && (
  • - e.preventDefault()} className={styles.placeholder}> + Flash
  • @@ -149,17 +157,17 @@ const BoardsList = ({ multisub }: { multisub: MultisubSubplebbit[] }) => { {(showAll || showWorksafeOnly) && ( <>
  • - e.preventDefault()} className={styles.placeholder}> + Transportation
  • - e.preventDefault()} className={styles.placeholder}> + Otaku Culture
  • - e.preventDefault()} className={styles.placeholder}> + Virtual YouTubers
  • @@ -175,42 +183,42 @@ const BoardsList = ({ multisub }: { multisub: MultisubSubplebbit[] }) => {

    Video Games

    • - e.preventDefault()} className={styles.placeholder}> + Video Games
    • - e.preventDefault()} className={styles.placeholder}> + Video Game Generals
    • - e.preventDefault()} className={styles.placeholder}> + Video Games/Multiplayer
    • - e.preventDefault()} className={styles.placeholder}> + Video Games/Mobile
    • - e.preventDefault()} className={styles.placeholder}> + Pokémon
    • - e.preventDefault()} className={styles.placeholder}> + Retro Games
    • - e.preventDefault()} className={styles.placeholder}> + Video Games/RPG
    • - e.preventDefault()} className={styles.placeholder}> + Video Games/Strategy
    • @@ -226,77 +234,77 @@ const BoardsList = ({ multisub }: { multisub: MultisubSubplebbit[] }) => {

      Interests

      • - e.preventDefault()} className={styles.placeholder}> + Comics & Cartoons
      • - e.preventDefault()} className={styles.placeholder}> + Technology
      • - e.preventDefault()} className={styles.placeholder}> + Television & Film
      • - e.preventDefault()} className={styles.placeholder}> + Weapons
      • - e.preventDefault()} className={styles.placeholder}> + Auto
      • - e.preventDefault()} className={styles.placeholder}> + Animals & Nature
      • - e.preventDefault()} className={styles.placeholder}> + Traditional Games
      • - e.preventDefault()} className={styles.placeholder}> + Sports
      • - e.preventDefault()} className={styles.placeholder}> + Extreme Sports
      • - e.preventDefault()} className={styles.placeholder}> + Professional Wrestling
      • - e.preventDefault()} className={styles.placeholder}> + Science & Math
      • - e.preventDefault()} className={styles.placeholder}> + History & Humanities
      • - e.preventDefault()} className={styles.placeholder}> + International
      • - e.preventDefault()} className={styles.placeholder}> + Outdoors
      • - e.preventDefault()} className={styles.placeholder}> + Toys
      • @@ -311,7 +319,7 @@ const BoardsList = ({ multisub }: { multisub: MultisubSubplebbit[] }) => {
          {(showAll || showNsfwOnly) && (
        • - e.preventDefault()} className={styles.placeholder}> + Oekaki
        • @@ -319,17 +327,17 @@ const BoardsList = ({ multisub }: { multisub: MultisubSubplebbit[] }) => { {(showAll || showWorksafeOnly) && ( <>
        • - e.preventDefault()} className={styles.placeholder}> + Papercraft & Origami
        • - e.preventDefault()} className={styles.placeholder}> + Photography
        • - e.preventDefault()} className={styles.placeholder}> + Food & Cooking
        • @@ -337,14 +345,14 @@ const BoardsList = ({ multisub }: { multisub: MultisubSubplebbit[] }) => { )} {(showAll || showNsfwOnly) && (
        • - e.preventDefault()} className={styles.placeholder}> + Artwork/Critique
        • )} {(showAll || showNsfwOnly) && (
        • - e.preventDefault()} className={styles.placeholder}> + Wallpapers/General
        • @@ -352,42 +360,42 @@ const BoardsList = ({ multisub }: { multisub: MultisubSubplebbit[] }) => { {(showAll || showWorksafeOnly) && ( <>
        • - e.preventDefault()} className={styles.placeholder}> + Literature
        • - e.preventDefault()} className={styles.placeholder}> + Music
        • - e.preventDefault()} className={styles.placeholder}> + Fashion
        • - e.preventDefault()} className={styles.placeholder}> + 3DCG
        • - e.preventDefault()} className={styles.placeholder}> + Graphic Design
        • - e.preventDefault()} className={styles.placeholder}> + Do-It-Yourself
        • - e.preventDefault()} className={styles.placeholder}> + Worksafe GIF
        • - e.preventDefault()} className={styles.placeholder}> + Quests
        • @@ -413,53 +421,53 @@ const BoardsList = ({ multisub }: { multisub: MultisubSubplebbit[] }) => { ) : (
        • - e.preventDefault()} className={styles.placeholder}> + Business & Finance
        • )}
        • - e.preventDefault()} className={styles.placeholder}> + Travel
        • - e.preventDefault()} className={styles.placeholder}> + Fitness
        • - e.preventDefault()} className={styles.placeholder}> + Paranormal
        • - e.preventDefault()} className={styles.placeholder}> + Advice
        • - e.preventDefault()} className={styles.placeholder}> + LGBT
        • - e.preventDefault()} className={styles.placeholder}> + Pony
        • - e.preventDefault()} className={styles.placeholder}> + Current News
        • - e.preventDefault()} className={styles.placeholder}> + Worksafe Requests
        • - e.preventDefault()} className={styles.placeholder}> + Very Important Posts
        • @@ -474,12 +482,12 @@ const BoardsList = ({ multisub }: { multisub: MultisubSubplebbit[] }) => {
          • - e.preventDefault()} className={styles.placeholder}> + Random
          • - e.preventDefault()} className={styles.placeholder}> + ROBOT9001
          • @@ -491,23 +499,23 @@ const BoardsList = ({ multisub }: { multisub: MultisubSubplebbit[] }) => { ) : (
          • - e.preventDefault()} className={styles.placeholder}> + Politically Incorrect
          • )}
          • - e.preventDefault()} className={styles.placeholder}> + International/Random
          • - e.preventDefault()} className={styles.placeholder}> + Cams & Meetups
          • - e.preventDefault()} className={styles.placeholder}> + Shit 4chan Says
          • @@ -524,67 +532,67 @@ const BoardsList = ({ multisub }: { multisub: MultisubSubplebbit[] }) => {
            • - e.preventDefault()} className={styles.placeholder}> + Sexy Beautiful Women
            • - e.preventDefault()} className={styles.placeholder}> + Hardcore
            • - e.preventDefault()} className={styles.placeholder}> + Handsome Men
            • - e.preventDefault()} className={styles.placeholder}> + Hentai
            • - e.preventDefault()} className={styles.placeholder}> + Ecchi
            • - e.preventDefault()} className={styles.placeholder}> + Yuri
            • - e.preventDefault()} className={styles.placeholder}> + Hentai/Alternative
            • - e.preventDefault()} className={styles.placeholder}> + Yaoi
            • - e.preventDefault()} className={styles.placeholder}> + Torrents
            • - e.preventDefault()} className={styles.placeholder}> + High Resolution
            • - e.preventDefault()} className={styles.placeholder}> + Adult GIF
            • - e.preventDefault()} className={styles.placeholder}> + Adult Cartoons
            • - e.preventDefault()} className={styles.placeholder}> + Adult Requests
            • diff --git a/src/views/home/home.tsx b/src/views/home/home.tsx index 093233f3..2e72b823 100644 --- a/src/views/home/home.tsx +++ b/src/views/home/home.tsx @@ -10,6 +10,7 @@ import BoardsList from './boards-list'; import Version from '../../components/version'; import useDisclaimerModalStore from '../../stores/use-disclaimer-modal-store'; import DisclaimerModal from '../../components/disclaimer-modal'; +import DirectoryModal from '../../components/directory-modal'; import _ from 'lodash'; // https://github.com/plebbit/lists/blob/master/5chan-multisub.json @@ -186,6 +187,7 @@ const Home = () => { return ( <> +
              From ace4b0c8db0ee3dcc9e435a2988134932bfd325b Mon Sep 17 00:00:00 2001 From: plebeius Date: Thu, 6 Nov 2025 16:37:09 +0100 Subject: [PATCH 09/12] fix(directory-modal): move to global layout and close on navigation --- src/app.tsx | 9 +++++++++ src/views/home/home.tsx | 2 -- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/app.tsx b/src/app.tsx index 5ae3aea6..d4e83417 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -4,6 +4,7 @@ import { useAccountComment } from '@plebbit/plebbit-react-hooks'; import { initSnow, removeSnow } from './lib/snow'; import { isAllView, isModView, isSubscriptionsView } from './lib/utils/view-utils'; import useReplyModalStore from './stores/use-reply-modal-store'; +import useDirectoryModalStore from './stores/use-directory-modal-store'; import useSpecialThemeStore from './stores/use-special-theme-store'; import useIsMobile from './hooks/use-is-mobile'; import useTheme from './hooks/use-theme'; @@ -18,6 +19,7 @@ import Post from './views/post'; import { DesktopBoardButtons, MobileBoardButtons } from './components/board-buttons'; import BoardHeader from './components/board-header'; import ChallengeModal from './components/challenge-modal'; +import DirectoryModal from './components/directory-modal'; import ReplyModal from './components/reply-modal'; import PostForm from './components/post-form'; import SubplebbitStats from './components/subplebbit-stats'; @@ -93,13 +95,20 @@ const GlobalLayout = () => { }, [theme]); const { activeCid, threadCid, subplebbitAddress, closeModal, showReplyModal, scrollY } = useReplyModalStore(); + const { closeDirectoryModal } = useDirectoryModalStore(); const location = useLocation(); const isInSettingsView = location.pathname.endsWith('/settings'); + // Close directory modal when navigating to a different page + useEffect(() => { + closeDirectoryModal(); + }, [location.pathname, closeDirectoryModal]); + return ( <> + {activeCid && threadCid && subplebbitAddress && ( { return ( <> -
              From 5f06fdf461788453c02293c9b9b6457f5f52c786 Mon Sep 17 00:00:00 2001 From: plebeius Date: Thu, 6 Nov 2025 17:07:26 +0100 Subject: [PATCH 10/12] feat(ui): add context-aware directory modal with different content for placeholder vs create board Add modal context tracking to show different content when opened from placeholder board clicks vs create board button. Include information about Subscribe button and pubsub voting discovery pages. Update README with comprehensive explanation of 5chan's competitive directory system and value proposition. --- README.md | 116 ++++++++++++++---- .../directory-modal/directory-modal.tsx | 76 ++++++++---- src/components/topbar/topbar.tsx | 2 +- src/stores/use-directory-modal-store.ts | 9 +- src/views/home/boards-list/boards-list.tsx | 2 +- 5 files changed, 150 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 522a649f..1f32cff5 100644 --- a/README.md +++ b/README.md @@ -4,40 +4,106 @@ _Telegram group for this repo https://t.me/fivechandev_ # 5chan -5chan is a serverless, adminless, decentralized and open-source 4chan alternative, where anyone can create and cryptographically own unlimited boards. All boards are selfhosted by their owners, who run [plebbit nodes](https://github.com/plebbit/plebbit-cli) that users connect to, peer-to-peer. All data is text-only, including links to load media/embeds. +5chan is a serverless, adminless, decentralized and open-source 4chan alternative built on the [Plebbit protocol](https://plebbit.com). It features the same directory structure as 4chan, but with a crucial difference: **anyone can create and own boards, and multiple boards can compete for each directory slot**. -- web version: https://5chan.app, also available using Brave/IPFS Companion on https://5chan.eth +## Key Features -### Downloads -- desktop version (full p2p plebbit node, seeds automatically): available for Mac/Windows/Linux, [download link in the release page](https://github.com/plebbit/5chan/releases/latest) -- mobile version: available for Android, [download link in the release page](https://github.com/plebbit/5chan/releases/latest) +### Decentralized Board Ownership +Unlike traditional imageboards, 5chan has no global admins or central authority. Anyone can create unlimited boards using their own [plebbit node](https://github.com/plebbit/plebbit-cli). Each board owner runs their own P2P node that users connect to peer-to-peer, giving them complete control over their board's content, moderation, and rules. -## How to create a board -In the plebbit protocol, a 5chan board is called a _subplebbit_. To run a subplebbit, you can choose between two options: +### Competitive Directory System +5chan maintains the familiar 4chan directory structure (Japanese Culture, Video Games, Interests, Creative, etc.), but introduces competition: **multiple boards can compete for each directory slot**. For example, there can be unlimited "Business & Finance" boards, but only the highest-voted one appears in the directory on the homepage. -1. If you prefer to use a **GUI**, download the desktop version of the Seedit client, available for Windows, MacOS and Linux: [latest release](https://github.com/plebbit/seedit/releases/latest). Create a subplebbit using using the familiar old.reddit-like UI, and modify its settings to your liking. The app runs an IPFS node, meaning you have to keep it running to have your board online. -2. If you prefer to use a **command line interface**, install plebbit-cli, available for Windows, MacOS and Linux: [latest release](https://github.com/plebbit/plebbit-cli/releases/latest). Follow the instructions in the readme of the repo. When running the daemon for the first time, it will output WebUI links you can use to manage your subplebbit with the ease of the GUI. +Currently, directory assignments are temporarily handpicked by developers through GitHub pull requests. In the future, this will be fully automated through **gasless pubsub voting** (see [Future Roadmap](#future-roadmap) below), making the process completely decentralized and community-driven. -Peers can connect to your subplebbit using any plebbit client, such as 5chan or Seedit. They only need the subplebbit's address, which is not stored in any central database, as plebbit is a pure peer-to-peer protocol. +### How It Works -### How to add a board to the boards list -The boards list on 5chan is plebbit's [lists](https://github.com/plebbit/lists) repository, specifically the [5chan-multisub.json](https://github.com/plebbit/lists/blob/master/5chan-multisub.json) file. You can open a pull request in that repo to add your subplebbit to the list, or contact devs via telegram [@plebbit](https://t.me/plebbit). In the future, this process will be automated by submitting proposals to a plebbit DAO, using the [plebbit token](https://etherscan.io/token/0xea81dab2e0ecbc6b5c4172de4c22b6ef6e55bd8f). +- **Current System**: Developers manually curate directory assignments by reviewing pull requests to the [5chan-multisub.json](https://github.com/plebbit/lists/blob/master/5chan-multisub.json) file. This is temporary until DAO curation is implemented. -## To run locally +- **Future System**: Directory board assignments will be determined through gasless voting using pubsub. Community members will vote on which board should be assigned to each directory, and the highest-voted board will automatically become the directory board. This creates a competitive marketplace where board quality and community engagement determine directory placement. -1. Install Node v22 (Download from https://nodejs.org) -2. Install Yarn: `npm install -g yarn` -3. `yarn install --frozen-lockfile` to install 5chan dependencies -4. `yarn start` to run the web client +- **Accessing Boards**: Users can access any board at any time using its address, regardless of directory assignment. Boards can be accessed via the search bar, by subscribing to them (which adds them to the top bar), or by directly navigating to their address. -### Scripts: +### Future Roadmap -- Web client: `yarn start` -- Electron client (must start web client first): `yarn electron` -- Electron client and don't delete data: `yarn electron:no-delete-data` -- Web client and electron client: `yarn electron:start` -- Web client and electron client and don't delete data: `yarn electron:start:no-delete-data` +The protocol design for pubsub voting is already drafted in [plebbit-js issue #25](https://github.com/plebbit/plebbit-js/issues/25). This will enable: +- Gasless voting using pubsub topics +- Weighted voting based on token balances +- Automatic directory resolution based on vote tallies +- Full decentralization without any intermediaries -### Build: +This feature is on the plebbit-js roadmap but hasn't been implemented yet. -The linux/windows/mac/android build scripts are in https://github.com/plebbit/5chan/blob/master/.github/workflows/release.yml +## Downloads + +- **Web version**: https://5chan.app (also available using Brave/IPFS Companion on https://5chan.eth) +- **Desktop version** (full P2P plebbit node, seeds automatically): Available for Mac/Windows/Linux, [download from the release page](https://github.com/plebbit/5chan/releases/latest) +- **Mobile version**: Available for Android, [download from the release page](https://github.com/plebbit/5chan/releases/latest) + +## Creating a Board + +In the plebbit protocol, a 5chan board is called a _subplebbit_. To create and run a subplebbit, you can choose between two options: + +### Option 1: Seedit GUI Client (Recommended for beginners) + +1. Download the desktop version of the Seedit client, available for Windows, macOS, and Linux: [latest release](https://github.com/plebbit/seedit/releases/latest) +2. Create a subplebbit using the familiar old.reddit-like UI +3. Modify its settings to your liking +4. Keep the app running to keep your board online (it runs an IPFS node) + +### Option 2: plebbit-cli Command Line Interface + +1. Install plebbit-cli, available for Windows, macOS, and Linux: [latest release](https://github.com/plebbit/plebbit-cli/releases/latest) +2. Follow the instructions in the repo's README +3. When running the daemon for the first time, it will output WebUI links you can use to manage your subplebbit with a GUI + +Once created, anyone can connect to your subplebbit using any plebbit client (such as 5chan or Seedit) by using the subplebbit's address. The address is not stored in any central database—plebbit is a pure peer-to-peer protocol. + +## Submitting Your Board to a Directory + +To have your board appear in a directory on the 5chan homepage: + +1. Ensure your board meets these requirements: + - Active and well-moderated + - Relevant to the directory category + - **99% uptime** (since a board acts like its own server—it's a P2P node) + +2. Open a pull request on GitHub by editing the [5chan-multisub.json](https://github.com/plebbit/lists/blob/master/5chan-multisub.json) file + +3. Add your board's entry with: + - Title + - Address + - NSFW status (if applicable) + +4. The developers will review your PR and merge it if approved + +**Note**: Even if your board isn't assigned to a directory, users can still access it at any time using its address. Directory assignment only affects visibility on the homepage. + +## Development + +### Prerequisites + +- Node.js v22 (Download from https://nodejs.org) +- Yarn: `npm install -g yarn` + +### Setup + +1. Clone the repository +2. Install dependencies: `yarn install --frozen-lockfile` +3. Start the web client: `yarn start` + +### Scripts + +- **Web client**: `yarn start` +- **Electron client** (must start web client first): `yarn electron` +- **Electron client** (don't delete data): `yarn electron:no-delete-data` +- **Web client and electron client**: `yarn electron:start` +- **Web client and electron client** (don't delete data): `yarn electron:start:no-delete-data` + +### Build + +The Linux/Windows/macOS/Android build scripts are in [.github/workflows/release.yml](https://github.com/plebbit/5chan/blob/master/.github/workflows/release.yml) + +## License + +5chan is open-source software (GPLv2 license) with no owner—anyone can host their own instance on any domain. The operator of any domain is merely hosting the web app and does not own, create, moderate, or control 5chan or any board content, which is stored peer-to-peer and generated by board owners and users. diff --git a/src/components/directory-modal/directory-modal.tsx b/src/components/directory-modal/directory-modal.tsx index 527b6d02..1e8ac149 100644 --- a/src/components/directory-modal/directory-modal.tsx +++ b/src/components/directory-modal/directory-modal.tsx @@ -2,7 +2,7 @@ import useDirectoryModalStore from '../../stores/use-directory-modal-store'; import styles from './directory-modal.module.css'; const DirectoryModal = () => { - const { showModal, closeDirectoryModal } = useDirectoryModalStore(); + const { showModal, modalContext, closeDirectoryModal } = useDirectoryModalStore(); if (!showModal) { return null; @@ -14,32 +14,38 @@ const DirectoryModal = () => { } }; + const isPlaceholderContext = modalContext === 'placeholder'; + return (
              -

              Submit a Board to a Directory

              +

              {isPlaceholderContext ? 'Submit a Board to a Directory' : 'Create a Board'}

              -

              - The board you clicked on doesn't exist yet, but it can be yours! -

              + {isPlaceholderContext && ( +

              + The board you clicked on doesn't exist yet, but it can be yours! +

              + )} -
              -

              What is a directory board vs a regular board?

              -

              - On 5chan, anyone can create and connect to any board using its address. A "directory board" is simply a board that has been assigned to a specific - directory category (like "Anime & Manga" or "Video Games") in the boards list on the homepage. This assignment is temporary and - handpicked by the devs until DAO curation is implemented. -

              -

              - Important: Anyone can create a board and users can access it at any time, regardless of whether it's assigned to a directory or not. - You can use the search bar on the homepage to connect to any board address peer-to-peer, even if it's not added as a directory. Additionally, every - board has a "[Subscribe]" button—when you subscribe to a board, it will appear in the top bar on the boards page, making it easy to access your - favorite boards regardless of directory assignment. -

              -
              + {isPlaceholderContext && ( +
              +

              What is a directory board vs a regular board?

              +

              + On 5chan, anyone can create and connect to any board using its address. A "directory board" is simply a board that has been assigned to a + specific directory category (like "Anime & Manga" or "Video Games") in the boards list on the homepage. This assignment is temporary + and handpicked by the devs until DAO curation is implemented. +

              +

              + Important: Anyone can create a board and users can access it at any time, regardless of whether it's assigned to a directory or not. + You can use the search bar on the homepage to connect to any board address peer-to-peer, even if it's not added as a directory. Additionally, every + board has a "[Subscribe]" button—when you subscribe to a board, it will appear in the top bar on the boards page, making it easy to access your + favorite boards regardless of directory assignment. +

              +
              + )}

              How to create your own board

              @@ -54,17 +60,25 @@ const DirectoryModal = () => { . Once created, anyone can connect to your board using its address, regardless of whether it's assigned to a directory or not.

              -
              - -
              -

              Requirements for directory assignment

              - To have your board assigned to a directory, it should be active, well-moderated, and relevant to the directory category. Most importantly, it should have - 99% uptime, since a board acts like its own server (it's a P2P node). The devs review submissions and reserve the right to approve or reject them based - on these criteria. + Building a following: Every board has a "[Subscribe]" button that users can click to subscribe to your board. When users + subscribe, your board will appear in the top bar on the boards page, making it easy for them to access it. This means you can build a following and have + active users even if your board isn't assigned to a directory or known by the devs. Users can discover your board through direct links, word of mouth, + or by searching for your board address.

              + {isPlaceholderContext && ( +
              +

              Requirements for directory assignment

              +

              + To have your board assigned to a directory, it should be active, well-moderated, and relevant to the directory category. Most importantly, it should have + 99% uptime, since a board acts like its own server (it's a P2P node). The devs review submissions and reserve the right to approve or reject them + based on these criteria. +

              +
              + )} +

              How to submit your board

              @@ -83,6 +97,16 @@ const DirectoryModal = () => { assigned to each directory, and the highest voted board will automatically become the directory board. This will make the process fully decentralized and community-driven.

              +

              + Discovery through voting pages: Each directory will have its own pubsub voting page that will serve as a discovery page for boards in that + category. Even low-voted or low-quality boards will appear in these voting lists, giving them visibility and helping them find users. This means boards + don't need to win the directory slot or be approved by devs to gain exposure—they can still be discovered and grow their subscriber base through the + voting pages. See the{' '} + + pubsub voting design draft + {' '} + for more details. +

              diff --git a/src/components/topbar/topbar.tsx b/src/components/topbar/topbar.tsx index 519f015c..5842ce4d 100644 --- a/src/components/topbar/topbar.tsx +++ b/src/components/topbar/topbar.tsx @@ -110,7 +110,7 @@ const TopBarDesktop = () => { )} [ - + openDirectoryModal('create-button')} style={{ cursor: 'pointer' }}> {t('create_board')} ] diff --git a/src/stores/use-directory-modal-store.ts b/src/stores/use-directory-modal-store.ts index 873aae9c..3ceed28a 100644 --- a/src/stores/use-directory-modal-store.ts +++ b/src/stores/use-directory-modal-store.ts @@ -1,17 +1,22 @@ import { create } from 'zustand'; +type ModalContext = 'placeholder' | 'create-button'; + interface DirectoryModalState { showModal: boolean; - openDirectoryModal: () => void; + modalContext: ModalContext; + openDirectoryModal: (context?: ModalContext) => void; closeDirectoryModal: () => void; } const useDirectoryModalStore = create((set) => ({ showModal: false, + modalContext: 'placeholder', - openDirectoryModal: () => { + openDirectoryModal: (context: ModalContext = 'placeholder') => { set({ showModal: true, + modalContext: context, }); }, diff --git a/src/views/home/boards-list/boards-list.tsx b/src/views/home/boards-list/boards-list.tsx index 121b9c7a..3c3ecb2b 100644 --- a/src/views/home/boards-list/boards-list.tsx +++ b/src/views/home/boards-list/boards-list.tsx @@ -49,7 +49,7 @@ const BoardsList = ({ multisub }: { multisub: MultisubSubplebbit[] }) => { // Handler for placeholder board links const handlePlaceholderClick = (e: React.MouseEvent) => { e.preventDefault(); - openDirectoryModal(); + openDirectoryModal('placeholder'); }; if (loading) { From 77d5fac5daa6567f5849ba1a47d2e0aa01169d17 Mon Sep 17 00:00:00 2001 From: plebeius Date: Thu, 6 Nov 2025 17:15:13 +0100 Subject: [PATCH 11/12] refactor(ui): make directory modal more concise and improve heading styling --- .../directory-modal.module.css | 3 +- .../directory-modal/directory-modal.tsx | 85 +++++++------------ 2 files changed, 35 insertions(+), 53 deletions(-) diff --git a/src/components/directory-modal/directory-modal.module.css b/src/components/directory-modal/directory-modal.module.css index c98312e8..c220b20f 100644 --- a/src/components/directory-modal/directory-modal.module.css +++ b/src/components/directory-modal/directory-modal.module.css @@ -89,9 +89,10 @@ .section h3 { margin: 0 0 8px 0; padding: 0; - font-size: 13px; + font-size: 14px; font-weight: bold; color: rgb(136, 0, 0); + letter-spacing: 0.3px; } .section p { diff --git a/src/components/directory-modal/directory-modal.tsx b/src/components/directory-modal/directory-modal.tsx index 1e8ac149..2239877f 100644 --- a/src/components/directory-modal/directory-modal.tsx +++ b/src/components/directory-modal/directory-modal.tsx @@ -32,89 +32,70 @@ const DirectoryModal = () => { {isPlaceholderContext && (
              -

              What is a directory board vs a regular board?

              +

              Directory vs Regular Board

              - On 5chan, anyone can create and connect to any board using its address. A "directory board" is simply a board that has been assigned to a - specific directory category (like "Anime & Manga" or "Video Games") in the boards list on the homepage. This assignment is temporary - and handpicked by the devs until DAO curation is implemented. -

              -

              - Important: Anyone can create a board and users can access it at any time, regardless of whether it's assigned to a directory or not. - You can use the search bar on the homepage to connect to any board address peer-to-peer, even if it's not added as a directory. Additionally, every - board has a "[Subscribe]" button—when you subscribe to a board, it will appear in the top bar on the boards page, making it easy to access your - favorite boards regardless of directory assignment. + A "directory board" is assigned to a category on the homepage (like "Anime & Manga" or "Video Games"). Directory assignments + are temporary and handpicked by devs until DAO curation is implemented.{' '} + Anyone can create a board and users can access it anytime using its address—use the search bar, direct links, or the + "[Subscribe]" button to access boards regardless of directory assignment.

              )}
              -

              How to create your own board

              +

              Creating Your Board

              - You can create a board using either the{' '} + Create a board using the{' '} Seedit GUI client {' '} - or the{' '} + or{' '} - plebbit-cli command line interface + plebbit-cli - . Once created, anyone can connect to your board using its address, regardless of whether it's assigned to a directory or not. -

              -

              - Building a following: Every board has a "[Subscribe]" button that users can click to subscribe to your board. When users - subscribe, your board will appear in the top bar on the boards page, making it easy for them to access it. This means you can build a following and have - active users even if your board isn't assigned to a directory or known by the devs. Users can discover your board through direct links, word of mouth, - or by searching for your board address. + . Build a following: 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.

              {isPlaceholderContext && (
              -

              Requirements for directory assignment

              +

              Directory Requirements

              - To have your board assigned to a directory, it should be active, well-moderated, and relevant to the directory category. Most importantly, it should have - 99% uptime, since a board acts like its own server (it's a P2P node). The devs review submissions and reserve the right to approve or reject them - based on these criteria. + Boards need 99% uptime (they're P2P nodes), plus be active, well-moderated, and relevant to the category. Devs review and approve/reject based on + these criteria.

              )}
              -

              How to submit your board

              +

              Submitting to a Directory

              - To request your board be added to a directory, open a pull request on GitHub by editing the{' '} + Open a PR editing{' '} - 5chan-multisub.json file - - . Add your board's entry with its title, address, and NSFW status (if applicable). The devs will review your PR and merge it if approved. -

              -
              - -
              -

              Future roadmap

              -

              - In the future, directory board assignments will be determined through gasless voting using pubsub. Community members will vote on which board should be - assigned to each directory, and the highest voted board will automatically become the directory board. This will make the process fully decentralized and - community-driven. -

              -

              - Discovery through voting pages: Each directory will have its own pubsub voting page that will serve as a discovery page for boards in that - category. Even low-voted or low-quality boards will appear in these voting lists, giving them visibility and helping them find users. This means boards - don't need to win the directory slot or be approved by devs to gain exposure—they can still be discovered and grow their subscriber base through the - voting pages. See the{' '} - - pubsub voting design draft + 5chan-multisub.json {' '} - for more details. + with your board's title, address, and NSFW status. Devs will review and merge if approved. 99% uptime is required.

              -

              Dev veto power

              +

              Future: DAO Voting

              - While the devs reserve the right to change directory resolutions, this is done through commits in the fully open-source repository. There is no centralized - or server-side action required—anyone can change the directory resolution by modifying the code and redeploying to their own domain. 5chan is adminless, and - the app itself has no owner or central authority. + Directory assignments will use gasless pubsub voting—communities vote, highest-voted board wins the slot. Voting pages = discovery: Each + directory's voting page lists all competing boards (even low-voted ones), giving visibility without winning or dev approval. See{' '} + + design draft + + . +

              +
              + +
              +

              Decentralization

              +

              + Devs can change directories via commits in the open-source repo. No centralized servers—anyone can fork, modify, and redeploy to their own domain. 5chan is + adminless with no central authority.

              From f44b9aefbb926ece4b432606a7f9f6ed1d9bcf39 Mon Sep 17 00:00:00 2001 From: plebeius Date: Thu, 6 Nov 2025 17:24:09 +0100 Subject: [PATCH 12/12] feat(board-header): prioritize default subplebbit title over store title --- src/components/board-header/board-header.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/board-header/board-header.tsx b/src/components/board-header/board-header.tsx index eb7b0a0b..69113722 100644 --- a/src/components/board-header/board-header.tsx +++ b/src/components/board-header/board-header.tsx @@ -6,7 +6,7 @@ import Plebbit from '@plebbit/plebbit-js'; import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits'; import { isAllView, isSubscriptionsView, isModView } from '../../lib/utils/view-utils'; import styles from './board-header.module.css'; -import { useMultisubMetadata } from '../../hooks/use-default-subplebbits'; +import { useMultisubMetadata, useDefaultSubplebbits } from '../../hooks/use-default-subplebbits'; import useIsMobile from '../../hooks/use-is-mobile'; import useIsSubplebbitOffline from '../../hooks/use-is-subplebbit-offline'; import { shouldShowSnow } from '../../lib/snow'; @@ -35,13 +35,18 @@ const BoardHeader = () => { const { address, shortAddress } = subplebbit || {}; const multisubMetadata = useMultisubMetadata(); + const defaultSubplebbits = useDefaultSubplebbits(); + + // Find matching subplebbit from default list to get its title + const defaultSubplebbit = subplebbitAddress ? defaultSubplebbits.find((s) => s.address === subplebbitAddress) : null; + const title = isInAllView ? multisubMetadata?.title || 'all' : isInSubscriptionsView ? 'Subscriptions' : isInModView ? _.startCase(t('boards_you_moderate')) - : subplebbit?.title; + : defaultSubplebbit?.title || subplebbit?.title; const subtitle = isInAllView ? 'p/all' : isInSubscriptionsView ? 'p/subscriptions' : isInModView ? 'p/mod' : `p/${address}`; const { isOffline, isOnlineStatusLoading, offlineIconClass, offlineTitle } = useIsSubplebbitOffline(subplebbit);