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 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} />
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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