feat(rules): add rules page with board selector

This commit is contained in:
plebeius
2025-12-29 00:26:15 +01:00
parent 5aa1eaf13a
commit d800ee9817
5 changed files with 366 additions and 0 deletions
+2
View File
@@ -13,6 +13,7 @@ import { getSubplebbitAddress, isPostRoute, isPendingPostRoute } from './lib/uti
import styles from './app.module.css'; import styles from './app.module.css';
import FAQ from './views/faq'; import FAQ from './views/faq';
import Home from './views/home'; import Home from './views/home';
import Rules from './views/rules';
import NotFound from './views/not-found'; import NotFound from './views/not-found';
import PendingPost from './views/pending-post'; import PendingPost from './views/pending-post';
import Post from './views/post'; import Post from './views/post';
@@ -145,6 +146,7 @@ const App = () => (
<Route element={<GlobalLayout />}> <Route element={<GlobalLayout />}>
<Route path='/' element={<Home />} /> <Route path='/' element={<Home />} />
<Route path='/faq' element={<FAQ />} /> <Route path='/faq' element={<FAQ />} />
<Route path='/rules' element={<Rules />} />
<Route element={<BoardLayout />}> <Route element={<BoardLayout />}>
<Route path='/all/:timeFilterName?' element={null} /> <Route path='/all/:timeFilterName?' element={null} />
<Route path='/all/:timeFilterName?/settings' element={null} /> <Route path='/all/:timeFilterName?/settings' element={null} />
+3
View File
@@ -133,6 +133,9 @@ export const Footer = () => {
<li> <li>
<Link to='/faq'>FAQ</Link> <Link to='/faq'>FAQ</Link>
</li> </li>
<li>
<Link to='/rules'>Rules</Link>
</li>
<li> <li>
<a href='https://t.me/plebbit' target='_blank' rel='noopener noreferrer'> <a href='https://t.me/plebbit' target='_blank' rel='noopener noreferrer'>
Telegram Telegram
+1
View File
@@ -0,0 +1 @@
export { default } from './rules';
+174
View File
@@ -0,0 +1,174 @@
.wrapper {
width: 100%;
}
.content {
width: 100%;
height: 100%;
margin: auto;
text-align: left;
width: 750px;
min-width: 750px;
}
.box {
position: relative;
margin-bottom: 0.5em;
padding-bottom: 0.5em;
border: 1px solid;
}
.boxBar {
padding-left: 0.5em;
line-height: 2em;
text-transform: capitalize;
}
.boxBar h2 {
font-size: 131%;
font-weight: 700;
}
.boxContent {
line-height: 1.5em;
font-size: 93%;
padding: 0.5em;
padding-top: 0.25em;
padding-bottom: 0;
line-height: 1.5em;
color: #000;
}
.infoBox {
background: var(--homepage-box-background-color);
color: var(--homepage-infobox-text-color);
border: 1px solid var(--homepage-box-border-color);
}
.infoBox .boxBar {
background: var(--homepage-box-bar-background-color);
color: var(--homepage-box-bar-text-color);
}
.selectorBox {
background: #efe;
color: #060;
}
.selectorBox .boxBar {
background: #9c6;
color: #060;
}
.selectorRow {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
box-sizing: border-box;
}
.boardSelect {
padding: 5px 10px;
font-size: 14px;
border: 1px solid #aaa;
border-radius: 0;
background: #fff;
cursor: pointer;
min-width: 200px;
box-sizing: border-box;
}
.orSeparator {
color: #666;
font-style: italic;
}
.customAddressForm {
display: flex;
align-items: center;
gap: 5px;
box-sizing: border-box;
}
.addressInput {
padding: 5px 10px;
font-size: 14px;
border: 1px solid #aaa;
border-radius: 0;
min-width: 200px;
box-sizing: border-box;
}
.addressInput:focus {
border-color: #ea8;
outline: none;
}
.goButton {
padding: 5px 10px;
font-size: 14px;
cursor: pointer;
box-sizing: border-box;
}
.rulesBox {
background: #eff;
color: #006;
}
.rulesBox .boxBar {
background: #59a;
color: #fff;
}
.box ol {
margin: 1em;
margin-left: 2em;
}
.box ol li {
list-style: decimal outside;
margin-bottom: 0.5em;
}
@media (max-width: 640px) {
.content {
min-width: 0;
width: auto;
padding: 10px;
}
.selectorRow {
flex-direction: column;
align-items: stretch;
}
.boardSelect,
.addressInput {
width: 100%;
min-width: 0;
box-sizing: border-box;
}
.orSeparator {
text-align: center;
}
.customAddressForm {
flex-direction: column;
width: 100%;
box-sizing: border-box;
}
.goButton {
box-sizing: border-box;
margin-top: 5px;
padding: 5px 10px;
cursor: pointer;
}
}
.selectorBoxTitle, .rulesBoxTitle {
text-transform: none;
}
+186
View File
@@ -0,0 +1,186 @@
import { useEffect, useState, FormEvent } from 'react';
import { useSubplebbit } from '@plebbit/plebbit-react-hooks';
import { Footer, HomeLogo } from '../home';
import { useDefaultSubplebbits, MultisubSubplebbit } from '../../hooks/use-default-subplebbits';
import styles from './rules.module.css';
const getBoardShortCode = (title?: string): string => {
if (!title) return '';
const match = title.match(/^\/([^/]+)\//);
return match ? match[1] : '';
};
const getBoardName = (title?: string): string => {
if (!title) return '';
const match = title.match(/^\/[^/]+\/\s*-\s*(.+)$/);
return match ? match[1] : title;
};
const BoardRulesDisplay = ({ subplebbitAddress, defaultSubplebbits }: { subplebbitAddress: string; defaultSubplebbits: MultisubSubplebbit[] }) => {
const subplebbit = useSubplebbit({ subplebbitAddress });
const { rules, state, title, shortAddress } = subplebbit || {};
let loadingText: string | null = null;
if (!subplebbit) {
loadingText = 'connecting...';
} else {
switch (state) {
case 'fetching-ipns':
case 'fetching-ipfs':
loadingText = 'loading...';
break;
case 'failed':
loadingText = 'failed to load';
break;
case 'succeeded':
loadingText = null;
break;
default:
loadingText = state ? `${state}...` : 'loading...';
}
}
const isLoaded = state === 'succeeded';
const defaultSub = defaultSubplebbits.find((sub) => sub.address === subplebbitAddress);
let displayTitle: string;
if (defaultSub?.title) {
const shortCode = getBoardShortCode(defaultSub.title);
const boardName = getBoardName(defaultSub.title);
displayTitle = `Rules for: /${shortCode}/ - ${boardName}`;
} else if (title) {
const shortCode = getBoardShortCode(title);
const boardName = getBoardName(title);
if (shortCode && boardName && boardName !== title) {
displayTitle = `Rules for: /${shortCode}/ - ${boardName}`;
} else {
displayTitle = `Rules for: ${shortAddress || subplebbitAddress}`;
}
} else {
displayTitle = `Rules for: ${shortAddress || subplebbitAddress}`;
}
return (
<div className={`${styles.box} ${styles.rulesBox}`}>
<div className={styles.boxBar}>
<h2 className={styles.rulesBoxTitle}>{displayTitle}</h2>
</div>
<div className={styles.boxContent}>
{!isLoaded ? (
<p>
<em>{loadingText}</em>
</p>
) : rules && rules.length > 0 ? (
<ol>
{rules.map((rule, index) => (
<li key={index}>{rule}</li>
))}
</ol>
) : (
<p>
<em>This board has no rules set by its owner.</em>
</p>
)}
</div>
</div>
);
};
const BoardSelector = ({
defaultSubplebbits,
selectedAddress,
onSelect,
}: {
defaultSubplebbits: MultisubSubplebbit[];
selectedAddress: string;
onSelect: (address: string) => void;
}) => {
const [customAddress, setCustomAddress] = useState('');
const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value;
if (value) {
onSelect(value);
setCustomAddress('');
}
};
const handleCustomSubmit = (e: FormEvent) => {
e.preventDefault();
if (customAddress.trim()) {
onSelect(customAddress.trim());
}
};
return (
<div className={`${styles.box} ${styles.selectorBox}`}>
<div className={styles.boxBar}>
<h2 className={styles.selectorBoxTitle}>Load rules from a board</h2>
</div>
<div className={styles.boxContent}>
<div className={styles.selectorRow}>
<select value={selectedAddress} onChange={handleSelectChange} className={styles.boardSelect}>
<option value=''>Select board...</option>
{defaultSubplebbits.map((sub) => {
const shortCode = getBoardShortCode(sub.title);
const boardName = getBoardName(sub.title);
return (
<option key={sub.address} value={sub.address}>
/{shortCode}/ - {boardName}
</option>
);
})}
</select>
<span className={styles.orSeparator}>or</span>
<form onSubmit={handleCustomSubmit} className={styles.customAddressForm}>
<input
type='text'
placeholder='enter board address'
value={customAddress}
onChange={(e) => setCustomAddress(e.target.value)}
className={styles.addressInput}
/>
<button type='submit' className={styles.goButton}>
Go
</button>
</form>
</div>
</div>
</div>
);
};
const Rules = () => {
const defaultSubplebbits = useDefaultSubplebbits();
const [selectedAddress, setSelectedAddress] = useState('');
useEffect(() => {
window.scrollTo(0, 0);
document.title = 'Rules - 5chan';
}, []);
return (
<div className={styles.wrapper}>
<div className={styles.content}>
<HomeLogo />
<div className={`${styles.box} ${styles.infoBox}`}>
<div className={styles.boxBar}>
<h2>Rules</h2>
</div>
<div className={styles.boxContent}>
5chan is a serverless and adminless tool to browse and post to decentralized imageboards. 5chan does not have global rules or moderators. Each board is
independently owned and moderated, with its own set of rules determined by the board owner.
<br />
<br />
Please read and respect the rules of whatever board you decide to post to.
</div>
</div>
<BoardSelector defaultSubplebbits={defaultSubplebbits} selectedAddress={selectedAddress} onSelect={setSelectedAddress} />
{selectedAddress && <BoardRulesDisplay subplebbitAddress={selectedAddress} defaultSubplebbits={defaultSubplebbits} />}
<Footer />
</div>
</div>
);
};
export default Rules;