refactor(modals): split directory modal into separate create board modal

Separate DirectoryModal (for home view placeholders) and CreateBoardModal (for board view create button) into distinct components with their own stores, allowing independent styling and cleaner separation of concerns.
This commit is contained in:
plebeius
2025-11-06 22:20:46 +01:00
parent f44b9aefbb
commit 79dcc73556
10 changed files with 262 additions and 49 deletions
+9 -9
View File
@@ -4,7 +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 useCreateBoardModalStore from './stores/use-create-board-modal-store';
import useSpecialThemeStore from './stores/use-special-theme-store';
import useIsMobile from './hooks/use-is-mobile';
import useTheme from './hooks/use-theme';
@@ -19,7 +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 CreateBoardModal from './components/create-board-modal';
import ReplyModal from './components/reply-modal';
import PostForm from './components/post-form';
import SubplebbitStats from './components/subplebbit-stats';
@@ -34,6 +34,7 @@ const BoardLayout = () => {
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
const isInModView = isModView(location.pathname);
const pendingPost = useAccountComment({ commentIndex: accountCommentIndex ? parseInt(accountCommentIndex) : undefined });
const { closeCreateBoardModal } = useCreateBoardModalStore();
// Christmas theme
const { isEnabled: isSpecialEnabled } = useSpecialThemeStore();
@@ -46,6 +47,11 @@ const BoardLayout = () => {
};
}, [isSpecialEnabled, isMobile]);
// Close create board modal when navigating to a different page
useEffect(() => {
closeCreateBoardModal();
}, [location.pathname, closeCreateBoardModal]);
// force rerender of post form when navigating between pages, except when opening settings modal in current view
const key = location.pathname.endsWith('/settings')
? `${subplebbitAddress}-${location.pathname.replace(/\/settings$/, '')}`
@@ -54,6 +60,7 @@ const BoardLayout = () => {
return (
<div className={styles.boardLayout}>
<TopBar />
<CreateBoardModal />
<BoardHeader />
{isMobile
? (subplebbitAddress || isInAllView || isInModView || isInSubscriptionsView || pendingPost?.subplebbitAddress) && (
@@ -95,20 +102,13 @@ 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 (
<>
<ChallengeModal />
<DirectoryModal />
{activeCid && threadCid && subplebbitAddress && (
<ReplyModal
closeModal={closeModal}
@@ -0,0 +1,109 @@
.backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(128, 128, 128, 0.5);
z-index: 999;
}
.createBoardDialog {
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) {
.createBoardDialog {
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;
}
.section {
margin-bottom: 20px;
}
.section:last-child {
margin-bottom: 0;
}
.section h3 {
margin: 0 0 8px 0;
padding: 0;
font-size: 14px;
font-weight: bold;
color: rgb(136, 0, 0);
letter-spacing: 0.3px;
}
.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;
}
.createBoardFooter {
padding: 10px;
display: flex;
justify-content: center;
gap: 10px;
}
@@ -0,0 +1,80 @@
import useCreateBoardModalStore from '../../stores/use-create-board-modal-store';
import styles from './create-board-modal.module.css';
const CreateBoardModal = () => {
const { showModal, closeCreateBoardModal } = useCreateBoardModalStore();
if (!showModal) {
return null;
}
const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (e.target === e.currentTarget) {
closeCreateBoardModal();
}
};
return (
<div className={styles.backdrop} onClick={handleBackdropClick}>
<div className={styles.createBoardDialog}>
<div className={styles.hd}>
<h2>Create a Board</h2>
<button className={styles.closeButton} onClick={closeCreateBoardModal} title='Close' />
</div>
<div className={styles.bd}>
<div className={styles.section}>
<h3>Creating Your Board</h3>
<p>
Create a board using the{' '}
<a href='https://plebbit.github.io/docs/learn/clients/5chan/create-a-board' target='_blank' rel='noopener noreferrer'>
Seedit GUI client
</a>{' '}
or{' '}
<a href='https://github.com/plebbit/plebbit-cli' target='_blank' rel='noopener noreferrer'>
plebbit-cli
</a>
. <strong>Build a following:</strong> Users can subscribe to your board via the &quot;[Subscribe]&quot; button, which adds it to their top bar. You can gain
subscribers through direct links, word of mouth, or searchno directory assignment or dev approval needed.
</p>
</div>
<div className={styles.section}>
<h3>Submitting to a Directory</h3>
<p>
Open a PR editing{' '}
<a href='https://github.com/plebbit/lists/blob/master/5chan-multisub.json' target='_blank' rel='noopener noreferrer'>
5chan-multisub.json
</a>{' '}
with your board&apos;s title, address, and NSFW status. Devs will review and merge if approved. 99% uptime is required.
</p>
</div>
<div className={styles.section}>
<h3>Future: DAO Voting</h3>
<p>
Directory assignments will use gasless pubsub votingcommunities vote, highest-voted board wins the slot. <strong>Voting pages = discovery:</strong> Each
directory&apos;s voting page lists all competing boards (even low-voted ones), giving visibility without winning or dev approval. See{' '}
<a href='https://github.com/plebbit/plebbit-js/issues/25' target='_blank' rel='noopener noreferrer'>
design draft
</a>
.
</p>
</div>
<div className={styles.section}>
<h3>Decentralization</h3>
<p>
Devs can change directories via commits in the open-source repo. No centralized serversanyone can fork, modify, and redeploy to their own domain. 5chan is
adminless with no central authority.
</p>
</div>
</div>
<div className={styles.createBoardFooter}>
<button onClick={closeCreateBoardModal}>Close</button>
</div>
</div>
</div>
);
};
export default CreateBoardModal;
@@ -0,0 +1 @@
export { default } from './create-board-modal';
@@ -2,7 +2,7 @@ import useDirectoryModalStore from '../../stores/use-directory-modal-store';
import styles from './directory-modal.module.css';
const DirectoryModal = () => {
const { showModal, modalContext, closeDirectoryModal } = useDirectoryModalStore();
const { showModal, closeDirectoryModal } = useDirectoryModalStore();
if (!showModal) {
return null;
@@ -14,33 +14,27 @@ const DirectoryModal = () => {
}
};
const isPlaceholderContext = modalContext === 'placeholder';
return (
<div className={styles.backdrop} onClick={handleBackdropClick}>
<div className={styles.directoryDialog}>
<div className={styles.hd}>
<h2>{isPlaceholderContext ? 'Submit a Board to a Directory' : 'Create a Board'}</h2>
<h2>Submit a Board to a Directory</h2>
<button className={styles.closeButton} onClick={closeDirectoryModal} title='Close' />
</div>
<div className={styles.bd}>
{isPlaceholderContext && (
<p className={styles.introMessage}>
<strong>The board you clicked on doesn&apos;t exist yet, but it can be yours!</strong>
</p>
)}
<p className={styles.introMessage}>
<strong>The board you clicked on doesn&apos;t exist yet, but it can be yours!</strong>
</p>
{isPlaceholderContext && (
<div className={styles.section}>
<h3>Directory vs Regular Board</h3>
<p>
A &quot;directory board&quot; is assigned to a category on the homepage (like &quot;Anime & Manga&quot; or &quot;Video Games&quot;). Directory assignments
are temporary and handpicked by devs until DAO curation is implemented.{' '}
<strong>Anyone can create a board and users can access it anytime using its address</strong>use the search bar, direct links, or the
&quot;[Subscribe]&quot; button to access boards regardless of directory assignment.
</p>
</div>
)}
<div className={styles.section}>
<h3>Directory vs Regular Board</h3>
<p>
A &quot;directory board&quot; is assigned to a category on the homepage (like &quot;Anime & Manga&quot; or &quot;Video Games&quot;). Directory assignments
are temporary and handpicked by devs until DAO curation is implemented.{' '}
<strong>Anyone can create a board and users can access it anytime using its address</strong>use the search bar, direct links, or the
&quot;[Subscribe]&quot; button to access boards regardless of directory assignment.
</p>
</div>
<div className={styles.section}>
<h3>Creating Your Board</h3>
@@ -58,15 +52,13 @@ const DirectoryModal = () => {
</p>
</div>
{isPlaceholderContext && (
<div className={styles.section}>
<h3>Directory Requirements</h3>
<p>
Boards need 99% uptime (they&apos;re P2P nodes), plus be active, well-moderated, and relevant to the category. Devs review and approve/reject based on
these criteria.
</p>
</div>
)}
<div className={styles.section}>
<h3>Directory Requirements</h3>
<p>
Boards need 99% uptime (they&apos;re P2P nodes), plus be active, well-moderated, and relevant to the category. Devs review and approve/reject based on these
criteria.
</p>
</div>
<div className={styles.section}>
<h3>Submitting to a Directory</h3>
+3 -3
View File
@@ -6,7 +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 useCreateBoardModalStore from '../../stores/use-create-board-modal-store';
import styles from './topbar.module.css';
import _, { debounce } from 'lodash';
@@ -76,7 +76,7 @@ const TopBarDesktop = () => {
const params = useParams();
const isInCatalogView = isCatalogView(location.pathname, params);
const [showSearchBar, setShowSearchBar] = useState(false);
const { openDirectoryModal } = useDirectoryModalStore();
const { openCreateBoardModal } = useCreateBoardModalStore();
const subscriptions = account?.subscriptions;
@@ -110,7 +110,7 @@ const TopBarDesktop = () => {
</>
)}
[
<span className={styles.temporaryButton} onClick={() => openDirectoryModal('create-button')} style={{ cursor: 'pointer' }}>
<span className={styles.temporaryButton} onClick={() => openCreateBoardModal()} style={{ cursor: 'pointer' }}>
{t('create_board')}
</span>
]
@@ -0,0 +1,25 @@
import { create } from 'zustand';
interface CreateBoardModalState {
showModal: boolean;
openCreateBoardModal: () => void;
closeCreateBoardModal: () => void;
}
const useCreateBoardModalStore = create<CreateBoardModalState>((set) => ({
showModal: false,
openCreateBoardModal: () => {
set({
showModal: true,
});
},
closeCreateBoardModal: () => {
set({
showModal: false,
});
},
}));
export default useCreateBoardModalStore;
+2 -7
View File
@@ -1,22 +1,17 @@
import { create } from 'zustand';
type ModalContext = 'placeholder' | 'create-button';
interface DirectoryModalState {
showModal: boolean;
modalContext: ModalContext;
openDirectoryModal: (context?: ModalContext) => void;
openDirectoryModal: () => void;
closeDirectoryModal: () => void;
}
const useDirectoryModalStore = create<DirectoryModalState>((set) => ({
showModal: false,
modalContext: 'placeholder',
openDirectoryModal: (context: ModalContext = 'placeholder') => {
openDirectoryModal: () => {
set({
showModal: true,
modalContext: context,
});
},
+1 -1
View File
@@ -49,7 +49,7 @@ const BoardsList = ({ multisub }: { multisub: MultisubSubplebbit[] }) => {
// Handler for placeholder board links
const handlePlaceholderClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
openDirectoryModal('placeholder');
openDirectoryModal();
};
if (loading) {
+11
View File
@@ -9,7 +9,9 @@ import PopularThreadsBox from './popular-threads-box';
import BoardsList from './boards-list';
import Version from '../../components/version';
import useDisclaimerModalStore from '../../stores/use-disclaimer-modal-store';
import useDirectoryModalStore from '../../stores/use-directory-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
@@ -178,14 +180,23 @@ const Home = () => {
const defaultSubplebbits = useDefaultSubplebbits();
const subplebbitAddresses = useDefaultSubplebbitAddresses();
const { subplebbits } = useSubplebbits({ subplebbitAddresses });
const { closeDirectoryModal } = useDirectoryModalStore();
useEffect(() => {
document.title = '5chan';
}, []);
// Close directory modal when navigating away from home
useEffect(() => {
return () => {
closeDirectoryModal();
};
}, [closeDirectoryModal]);
return (
<>
<DisclaimerModal />
<DirectoryModal />
<div className={styles.content}>
<HomeLogo />
<SearchBar />