mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
feat(rules): add rules page with board selector
This commit is contained in:
@@ -13,6 +13,7 @@ import { getSubplebbitAddress, isPostRoute, isPendingPostRoute } from './lib/uti
|
||||
import styles from './app.module.css';
|
||||
import FAQ from './views/faq';
|
||||
import Home from './views/home';
|
||||
import Rules from './views/rules';
|
||||
import NotFound from './views/not-found';
|
||||
import PendingPost from './views/pending-post';
|
||||
import Post from './views/post';
|
||||
@@ -145,6 +146,7 @@ const App = () => (
|
||||
<Route element={<GlobalLayout />}>
|
||||
<Route path='/' element={<Home />} />
|
||||
<Route path='/faq' element={<FAQ />} />
|
||||
<Route path='/rules' element={<Rules />} />
|
||||
<Route element={<BoardLayout />}>
|
||||
<Route path='/all/:timeFilterName?' element={null} />
|
||||
<Route path='/all/:timeFilterName?/settings' element={null} />
|
||||
|
||||
@@ -133,6 +133,9 @@ export const Footer = () => {
|
||||
<li>
|
||||
<Link to='/faq'>FAQ</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to='/rules'>Rules</Link>
|
||||
</li>
|
||||
<li>
|
||||
<a href='https://t.me/plebbit' target='_blank' rel='noopener noreferrer'>
|
||||
Telegram
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './rules';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user